Repository: 11ty/eleventy Branch: main Commit: 0e8c596c63c8 Files: 997 Total size: 1.5 MB Directory structure: gitextract__ua1mjth/ ├── .editorconfig ├── .git-blame-ignore-revs ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ └── possible-bug.yml │ ├── dependabot.yml │ ├── opencollective.yml │ └── workflows/ │ ├── ci.yml │ ├── codeql.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cmd.cjs ├── docs/ │ ├── coverage.md │ ├── coverage.njk │ ├── eleventy.coverage.js │ └── release-instructions.md ├── eslint.config.js ├── package.json ├── packages/ │ └── client/ │ ├── README.md │ ├── generate-bundle.js │ ├── package.json │ ├── src/ │ │ ├── BundleCore.js │ │ ├── BundleEleventy.js │ │ ├── BundleI18nPlugin.js │ │ ├── BundleLiquid.js │ │ ├── BundleMarkdown.js │ │ ├── BundleNunjucks.js │ │ └── shims/ │ │ ├── process.cjs │ │ └── shim-core.js │ ├── test/ │ │ ├── client-core.test.js │ │ ├── client-eleventy.test.js │ │ └── shared-tests.js │ ├── update-package-json.js │ └── vitest.config.js ├── scripts/ │ ├── release-dryrun.sh │ └── release.sh ├── src/ │ ├── Adapters/ │ │ ├── Engines/ │ │ │ ├── Liquid.core.js │ │ │ ├── Liquid.js │ │ │ ├── Markdown.core.js │ │ │ ├── Markdown.js │ │ │ ├── Nunjucks.core.js │ │ │ └── Nunjucks.js │ │ ├── Packages/ │ │ │ ├── chalk.client.js │ │ │ ├── chalk.js │ │ │ ├── inspect.core.js │ │ │ ├── inspect.js │ │ │ ├── semver.client.js │ │ │ ├── semver.js │ │ │ ├── url.core.js │ │ │ └── url.js │ │ ├── getDefaultConfig.core.js │ │ └── getDefaultConfig.js │ ├── Benchmark/ │ │ ├── Benchmark.js │ │ ├── BenchmarkGroup.js │ │ └── BenchmarkManager.js │ ├── Core.js │ ├── CoreMinimal.js │ ├── Data/ │ │ ├── ComputedData.js │ │ ├── ComputedDataProxy.js │ │ ├── ComputedDataQueue.js │ │ ├── ComputedDataTemplateString.js │ │ ├── TemplateData.js │ │ └── TemplateDataInitialGlobalData.js │ ├── Eleventy.js │ ├── EleventyCommonJs.cjs │ ├── EleventyExtensionMap.js │ ├── EleventyFiles.js │ ├── EleventyServe.js │ ├── Engines/ │ │ ├── Custom.js │ │ ├── FrontMatter/ │ │ │ └── JavaScript.js │ │ ├── Html.js │ │ ├── JavaScript.js │ │ ├── Liquid.js │ │ ├── Markdown.js │ │ ├── Nunjucks.js │ │ ├── TemplateEngine.js │ │ ├── TemplateEngineManager.js │ │ └── Util/ │ │ └── ContextAugmenter.js │ ├── Errors/ │ │ ├── DuplicatePermalinkOutputError.js │ │ ├── EleventyBaseError.js │ │ ├── EleventyErrorHandler.js │ │ ├── EleventyErrorUtil.js │ │ ├── TemplateContentPrematureUseError.js │ │ ├── TemplateContentUnrenderedTemplateError.js │ │ └── UsingCircularTemplateContentReferenceError.js │ ├── EventBus.js │ ├── FileSystemSearch.js │ ├── Filters/ │ │ ├── GetCollectionItem.js │ │ ├── GetCollectionItemIndex.js │ │ ├── GetLocaleCollectionItem.js │ │ └── Url.js │ ├── GlobalDependencyMap.js │ ├── LayoutCache.js │ ├── Plugins/ │ │ ├── HtmlBasePlugin.js │ │ ├── HtmlRelativeCopyPlugin.js │ │ ├── I18nPlugin.js │ │ ├── IdAttributePlugin.js │ │ ├── InputPathToUrl.js │ │ ├── Pagination.js │ │ ├── PreserveClosingTagsPlugin.js │ │ └── RenderPlugin.js │ ├── Template.js │ ├── TemplateBehavior.js │ ├── TemplateCollection.js │ ├── TemplateConfig.js │ ├── TemplateContent.js │ ├── TemplateFileSlug.js │ ├── TemplateGlob.js │ ├── TemplateLayout.js │ ├── TemplateLayoutPathResolver.js │ ├── TemplateMap.js │ ├── TemplatePassthrough.js │ ├── TemplatePassthroughManager.js │ ├── TemplatePermalink.js │ ├── TemplatePreprocessors.js │ ├── TemplateRender.js │ ├── TemplateWriter.js │ ├── UserConfig.js │ ├── Util/ │ │ ├── ArrayUtil.js │ │ ├── AsyncEventEmitter.js │ │ ├── Compatibility.js │ │ ├── ConsoleLogger.js │ │ ├── DateParse.js │ │ ├── DirContains.js │ │ ├── EsmResolver.js │ │ ├── EsmResolverPortAdapter.core.js │ │ ├── EsmResolverPortAdapter.js │ │ ├── EventBusUtil.js │ │ ├── ExistsCache.js │ │ ├── FeatureTests.cjs │ │ ├── FeatureTests.core.cjs │ │ ├── FilePathUtil.js │ │ ├── FileSize.js │ │ ├── FileSystemManager.js │ │ ├── GetJavaScriptData.js │ │ ├── Git.js │ │ ├── GlobMatcher.client.js │ │ ├── GlobMatcher.js │ │ ├── GlobRemap.js │ │ ├── GlobStripper.js │ │ ├── HtmlRelativeCopy.js │ │ ├── HtmlTransformer.js │ │ ├── ImportJsonSync.js │ │ ├── IsAsyncFunction.js │ │ ├── JavaScriptDependencies.core.js │ │ ├── JavaScriptDependencies.js │ │ ├── MemoizeFunction.js │ │ ├── NewLineAdapter.core.js │ │ ├── NewLineAdapter.js │ │ ├── Objects/ │ │ │ ├── DeepFreeze.js │ │ │ ├── ObjectFilter.js │ │ │ ├── ProxyWrap.js │ │ │ ├── SampleModule.mjs │ │ │ ├── Sortable.js │ │ │ └── Unique.js │ │ ├── PassthroughCopyBehaviorCheck.js │ │ ├── PathNormalizer.js │ │ ├── PathPrefixer.js │ │ ├── Pluralize.js │ │ ├── ProjectDirectories.js │ │ ├── ProjectTemplateFormats.js │ │ ├── PromiseUtil.js │ │ ├── Require.js │ │ ├── RequireUtils.core.js │ │ ├── RequireUtils.js │ │ ├── ReservedData.js │ │ ├── ResolvePlugin.client.js │ │ ├── ResolvePlugin.js │ │ ├── RetrieveGlobals.client.js │ │ ├── RetrieveGlobals.core.js │ │ ├── RetrieveGlobals.js │ │ ├── SemverCoerce.js │ │ ├── SetUtil.js │ │ ├── TemplateDepGraph.js │ │ ├── TransformsUtil.js │ │ ├── TypeScript/ │ │ │ └── TypeScriptSample.cts │ │ ├── UrlUtil.js │ │ ├── importer.client.js │ │ ├── importer.core.js │ │ ├── importer.js │ │ ├── spawn.core.js │ │ └── spawn.js │ ├── Watch.js │ ├── WatchQueue.js │ ├── WatchTargets.js │ ├── defaultConfig.js │ ├── defaultConfigExtended.client.js │ └── defaultConfigExtended.js ├── test/ │ ├── ArrayUtilTest.js │ ├── BenchmarkTest.js │ ├── BundlePluginTest.js │ ├── CompatibilityTest.js │ ├── ComputedDataProxyTest.js │ ├── ComputedDataQueueTest.js │ ├── ComputedDataTemplateStringTest.js │ ├── ComputedDataTest.js │ ├── ConsoleLoggerTest.js │ ├── DependencyGraphTest.js │ ├── DirContainsTest.js │ ├── EleventyAddGlobalDataTest.js │ ├── EleventyErrorHandlerTest.js │ ├── EleventyErrorUtilTest.js │ ├── EleventyExtensionMapTest.js │ ├── EleventyFilesGitIgnoreEleventyIgnoreTest.js │ ├── EleventyFilesTest.js │ ├── EleventyImgTransformTest.js │ ├── EleventyMarkdownTest.js │ ├── EleventyNunjucksTest.js │ ├── EleventyServeTest.js │ ├── EleventyTest-CustomDateParsing.js │ ├── EleventyTest-PageData.js │ ├── EleventyTest-Preprocessors.js │ ├── EleventyTest-Shortcodes.js │ ├── EleventyTest.js │ ├── EleventyVirtualTemplatesTest.js │ ├── ExistsCacheTest.js │ ├── FileSystemSearchTest.js │ ├── GetCollectionItemIndexTest.js │ ├── GetCollectionItemTest.js │ ├── GlobRemapTest.js │ ├── GlobStripperTest.js │ ├── GlobalDependencyMapTest.js │ ├── HtmlBasePluginTest.js │ ├── HtmlRelativeCopyTest.js │ ├── I18nPluginTest.js │ ├── IdAttributePluginTest.js │ ├── ImportJsonSyncTest.js │ ├── InputPathToUrlPluginTest.js │ ├── Issue3467Test.js │ ├── Issue3788Test.js │ ├── Issue3797Test.js │ ├── Issue3808Test.js │ ├── Issue3809Test.js │ ├── Issue3816Test.js │ ├── Issue3818Test.js │ ├── Issue3823Test.js │ ├── Issue3825Test.js │ ├── Issue3831Test.js │ ├── Issue3833Test.js │ ├── Issue3850Test.js │ ├── Issue3853Test.js │ ├── Issue3854Test.js │ ├── Issue3860Test.js │ ├── Issue3870IncrementalTest.js │ ├── Issue3870Test.js │ ├── Issue3875Test.js │ ├── Issue434Test.js │ ├── Issue775Test.js │ ├── JavaScriptDependenciesTest.js │ ├── JavaScriptFrontMatterTest.js │ ├── LayoutCacheTest.js │ ├── LodashTest.js │ ├── PaginationTest.js │ ├── PassthroughCopyBehaviorTest.js │ ├── PathNormalizerTest.js │ ├── PathPrefixer.js │ ├── PluralizeTest.js │ ├── PreserveClosingTagsPluginTest.js │ ├── ProjectDirectoriesTest.js │ ├── ProjectTemplateFormatsTest.js │ ├── ProxyWrapTest.js │ ├── ReservedDataTest.js │ ├── SemverCheckTest.js │ ├── SortableTest.js │ ├── TemplateCollectionTest.js │ ├── TemplateConfigTest.js │ ├── TemplateDataTest.js │ ├── TemplateDepGraphTest.js │ ├── TemplateEngineManagerTest.js │ ├── TemplateEngineTest.js │ ├── TemplateFileSlugTest.js │ ├── TemplateGlobTest.js │ ├── TemplateLayoutPathResolverTest.js │ ├── TemplateLayoutTest.js │ ├── TemplateMapTest-ComputedData.js │ ├── TemplateMapTest.js │ ├── TemplatePassthroughManagerTest.js │ ├── TemplatePassthroughTest.js │ ├── TemplatePermalinkTest.js │ ├── TemplateRenderCustomTest.js │ ├── TemplateRenderHTMLTest.js │ ├── TemplateRenderJavaScriptTest.js │ ├── TemplateRenderLiquidTest.js │ ├── TemplateRenderMarkdownPluginTest.js │ ├── TemplateRenderMarkdownTest.js │ ├── TemplateRenderNunjucksTest.js │ ├── TemplateRenderPluginTest.js │ ├── TemplateRenderTest.js │ ├── TemplateTest-CompileOptions.js │ ├── TemplateTest-ComputedData.js │ ├── TemplateTest-CustomExtensions.js │ ├── TemplateTest-DataCascade.js │ ├── TemplateTest-Dates.js │ ├── TemplateTest-JavaScript.js │ ├── TemplateTest.js │ ├── TemplateTest_Permalink.js │ ├── TemplateWriterTest.js │ ├── TestUtilityTest.js │ ├── TransformsUtilTest.js │ ├── UrlTest.js │ ├── UserConfigTest.js │ ├── UserDataExtensionsTest.js │ ├── Util/ │ │ ├── normalizeNewLines.js │ │ └── normalizeSeparators.js │ ├── UtilSetUnionTest.js │ ├── WatchQueueTest.js │ ├── WatchTargetsTest.js │ ├── _getNewTemplateForTests.js │ ├── _getRenderedTemplates.js │ ├── _issues/ │ │ ├── 0/ │ │ │ ├── content/ │ │ │ │ └── index.html │ │ │ ├── eleventy.config.js │ │ │ └── issue-0-test.js │ │ ├── 2250/ │ │ │ ├── 2250-test.js │ │ │ ├── javascript.11ty.cjs │ │ │ ├── liquid.liquid │ │ │ └── nunjucks.njk │ │ ├── 3697/ │ │ │ ├── 3697-test.js │ │ │ └── _data/ │ │ │ └── folder/ │ │ │ ├── 0.json │ │ │ └── 3.json │ │ ├── 3809/ │ │ │ ├── .app/ │ │ │ │ ├── .eleventy.js │ │ │ │ └── _data/ │ │ │ │ └── app.json │ │ │ └── index.njk │ │ ├── 3853/ │ │ │ └── deeper/ │ │ │ └── index.njk │ │ ├── 3854/ │ │ │ ├── app/ │ │ │ │ ├── .eleventy.js │ │ │ │ └── index.njk │ │ │ └── index.njk │ │ ├── 3896/ │ │ │ ├── eleventy-input-folder/ │ │ │ │ ├── 3896.html │ │ │ │ └── _archive/ │ │ │ │ └── ignored.html │ │ │ └── test-files/ │ │ │ ├── eleventy.config.js │ │ │ └── issue3896-test.js │ │ ├── 3932/ │ │ │ ├── 1/ │ │ │ │ └── 2025.html │ │ │ ├── eleventy.config.js │ │ │ └── issue-3932-test.js │ │ └── 975/ │ │ ├── 975-test.js │ │ ├── another-post.md │ │ ├── index.md │ │ └── post.md │ ├── _testHelpers.js │ ├── cmdTest.js │ ├── file-system-search/ │ │ └── file.txt │ ├── noop/ │ │ └── .gitkeep │ ├── noop2/ │ │ └── .gitkeep │ ├── proxy-pagination-globaldata/ │ │ ├── _data/ │ │ │ └── banner.js │ │ ├── tmpl.liquid │ │ ├── tmpl2.njk │ │ └── tmpl4.11ty.js │ ├── semverCoerceTest.js │ ├── slugify-filter/ │ │ ├── comma.njk │ │ ├── multibyte.njk │ │ ├── slug-number.njk │ │ ├── slug-options.njk │ │ ├── slugify-number.njk │ │ ├── slugify-options.njk │ │ └── test.njk │ ├── stubs/ │ │ ├── .eleventyignore │ │ ├── 2016-02-01-permalinkdate.liquid │ │ ├── _data/ │ │ │ ├── globalData.json │ │ │ ├── globalData2.cjs │ │ │ ├── globalDataFn.js │ │ │ ├── globalDataFnCJS.cjs │ │ │ ├── subdir/ │ │ │ │ └── testDataSubdir.json │ │ │ ├── testData.json │ │ │ └── testDataLiquid.json │ │ ├── _includes/ │ │ │ ├── base.njk │ │ │ ├── custom-filter.liquid │ │ │ ├── default.liquid │ │ │ ├── defaultLayout.liquid │ │ │ ├── defaultLayoutLayoutContent.liquid │ │ │ ├── imports.njk │ │ │ ├── included-data.html │ │ │ ├── included-relative.njk │ │ │ ├── included.html │ │ │ ├── included.liquid │ │ │ ├── included.njk │ │ │ ├── included.nunj │ │ │ ├── layout-a.liquid │ │ │ ├── layout-b.liquid │ │ │ ├── layoutLiquid.liquid │ │ │ ├── layouts/ │ │ │ │ ├── div-wrapper-layout.njk │ │ │ │ ├── engineOverrides.njk │ │ │ │ ├── engineOverridesMd.njk │ │ │ │ ├── inasubdir.njk │ │ │ │ ├── issue-115.liquid │ │ │ │ ├── layout-contentdump.njk │ │ │ │ ├── layout-inherit-a.njk │ │ │ │ ├── layout-inherit-b.njk │ │ │ │ ├── layout-inherit-c.njk │ │ │ │ ├── post.liquid │ │ │ │ └── templateMapCollection.njk │ │ │ ├── multiple.liquid │ │ │ ├── multiple.md │ │ │ ├── mylocallayout.njk │ │ │ ├── permalink-data-layout.njk │ │ │ ├── permalink-in-layout/ │ │ │ │ ├── layout-fileslug.liquid │ │ │ │ └── layout.liquid │ │ │ ├── scopeleak.liquid │ │ │ ├── subfolder/ │ │ │ │ ├── included.html │ │ │ │ ├── included.liquid │ │ │ │ └── included.nunj │ │ │ └── test.js │ │ ├── _layouts/ │ │ │ └── layoutsdefault.liquid │ │ ├── add-extension/ │ │ │ ├── test.njk │ │ │ └── test.txt │ │ ├── broken-config.cjs │ │ ├── buffer.11ty.cjs │ │ ├── cfg-directories-export/ │ │ │ ├── eleventy.config.js │ │ │ └── src/ │ │ │ └── .gitkeep │ │ ├── cfg-directories-export-cjs/ │ │ │ ├── eleventy.config.cjs │ │ │ └── src/ │ │ │ └── .gitkeep │ │ ├── class-async-data-fn.11ty.cjs │ │ ├── class-async-filter.11ty.cjs │ │ ├── class-async.11ty.cjs │ │ ├── class-buffer.11ty.cjs │ │ ├── class-data-filter.11ty.cjs │ │ ├── class-data-fn-filter.11ty.cjs │ │ ├── class-data-fn-shorthand.11ty.cjs │ │ ├── class-data-fn.11ty.cjs │ │ ├── class-data-permalink-async-fn.11ty.cjs │ │ ├── class-data-permalink-buffer.11ty.cjs │ │ ├── class-data-permalink-fn-buffer.11ty.cjs │ │ ├── class-data-permalink-fn-filter.11ty.cjs │ │ ├── class-data-permalink-fn.11ty.cjs │ │ ├── class-data-permalink.11ty.cjs │ │ ├── class-data.11ty.cjs │ │ ├── class-filter.11ty.cjs │ │ ├── class-fns-has-page.11ty.cjs │ │ ├── class-fns.11ty.cjs │ │ ├── class-norender.11ty.cjs │ │ ├── class-with-dep-upstream.js │ │ ├── class-with-dep.11ty.cjs │ │ ├── class.11ty.cjs │ │ ├── classfields-data.11ty.cjs │ │ ├── cmd-help-processing/ │ │ │ └── _data/ │ │ │ └── test.js │ │ ├── collection/ │ │ │ ├── test1.md │ │ │ ├── test10.md │ │ │ ├── test2.md │ │ │ ├── test3.md │ │ │ ├── test4.md │ │ │ ├── test5.md │ │ │ ├── test6.html │ │ │ ├── test7.njk │ │ │ ├── test8.md │ │ │ └── test9.md │ │ ├── collection-layout/ │ │ │ ├── _includes/ │ │ │ │ └── layout.liquid │ │ │ ├── dog1.liquid │ │ │ └── template.liquid │ │ ├── collection-layout-wrap.njk │ │ ├── collection-slug/ │ │ │ ├── dog1.njk │ │ │ └── template.njk │ │ ├── collection-template/ │ │ │ ├── _includes/ │ │ │ │ └── layout.liquid │ │ │ ├── dog1.liquid │ │ │ └── template.liquid │ │ ├── collection2/ │ │ │ ├── test1.md │ │ │ └── test2.md │ │ ├── component/ │ │ │ ├── component.11tydata.cjs │ │ │ ├── component.11tydata.js │ │ │ ├── component.11tydata.json │ │ │ ├── component.json │ │ │ └── component.njk │ │ ├── component-async/ │ │ │ ├── component.11tydata.cjs │ │ │ ├── component.11tydata.js │ │ │ └── component.njk │ │ ├── config-deps-upstream.cjs │ │ ├── config-deps.cjs │ │ ├── config-empty-pathprefix.cjs │ │ ├── config-promise.js │ │ ├── config.cjs │ │ ├── custom-extension-no-permalink.txt │ │ ├── custom-extension.txt │ │ ├── custom-frontmatter/ │ │ │ ├── template-excerpt-comment.njk │ │ │ ├── template-newline1.njk │ │ │ ├── template-newline2.njk │ │ │ ├── template-newline3.njk │ │ │ ├── template-nonewline.njk │ │ │ ├── template-toml.njk │ │ │ └── template.njk │ │ ├── data-cascade/ │ │ │ ├── template.11tydata.cjs │ │ │ └── template.njk │ │ ├── datafiledoesnotexist/ │ │ │ └── template.njk │ │ ├── dates/ │ │ │ ├── 2018-01-01-file5.md │ │ │ ├── 2019-01-01-folder/ │ │ │ │ └── 2020-01-01-file.md │ │ │ ├── file1.md │ │ │ ├── file2.md │ │ │ ├── file2b.md │ │ │ ├── file3.md │ │ │ └── file4.md │ │ ├── default-class-export-and-others.11ty.js │ │ ├── default-export-and-others.11ty.js │ │ ├── default-frontmatter.txt │ │ ├── default-function-export-and-named-data.11ty.cjs │ │ ├── default-function-export-and-named-data.11ty.js │ │ ├── default-no-liquid.md │ │ ├── default.liquid │ │ ├── default.md │ │ ├── dependencies/ │ │ │ ├── dep1.cjs │ │ │ ├── dep2.cjs │ │ │ └── two-deps.11ty.cjs │ │ ├── deps/ │ │ │ ├── dep1.cjs │ │ │ └── dep2.cjs │ │ ├── dynamic-permalink/ │ │ │ └── test.njk │ │ ├── eleventyComputed/ │ │ │ ├── first.njk │ │ │ ├── override-reuse.njk │ │ │ ├── override.njk │ │ │ ├── permalink-simple.njk │ │ │ ├── permalink-slug.njk │ │ │ ├── permalink.njk │ │ │ ├── second.njk │ │ │ ├── third.njk │ │ │ ├── true.njk │ │ │ └── use-global-data.njk │ │ ├── eleventyExcludeFromCollections.njk │ │ ├── eleventyExcludeFromCollectionsPermalinkFalse.njk │ │ ├── engine-singletons/ │ │ │ ├── first.njk │ │ │ └── second.njk │ │ ├── exitCode/ │ │ │ └── failure.njk │ │ ├── exitCode_globalData/ │ │ │ ├── _data/ │ │ │ │ └── test.js │ │ │ └── test.liquid │ │ ├── exitCode_success/ │ │ │ └── success.njk │ │ ├── exports-flatdata.11ty.cjs │ │ ├── fileslug.11ty.cjs │ │ ├── firstdir/ │ │ │ └── seconddir/ │ │ │ └── component.njk │ │ ├── formatTest.liquid │ │ ├── frontmatter-date/ │ │ │ ├── test.liquid │ │ │ └── test.njk │ │ ├── function-arrow.11ty.cjs │ │ ├── function-async-filter.11ty.cjs │ │ ├── function-async.11ty.cjs │ │ ├── function-buffer.11ty.cjs │ │ ├── function-filter.11ty.cjs │ │ ├── function-fns.11ty.cjs │ │ ├── function-markdown.11ty.cjs │ │ ├── function-prototype.11ty.cjs │ │ ├── function-throws-async.11ty.cjs │ │ ├── function-throws.11ty.cjs │ │ ├── function.11ty.cjs │ │ ├── glob-pages/ │ │ │ ├── about.md │ │ │ ├── contact.md │ │ │ └── home.md │ │ ├── global-dash-variable.liquid │ │ ├── globby/ │ │ │ ├── _includes/ │ │ │ │ └── include.html │ │ │ └── test.html │ │ ├── ignore-dedupe/ │ │ │ └── .gitignore │ │ ├── ignore1/ │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignore2/ │ │ │ ├── .gitignore │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignore3/ │ │ │ ├── .eleventyignore │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignore4/ │ │ │ ├── .eleventyignore │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignore5/ │ │ │ ├── .gitignore │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignore6/ │ │ │ ├── .eleventyignore │ │ │ ├── .gitignore │ │ │ └── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignoredFolder/ │ │ │ └── ignored.md │ │ ├── ignorelocalroot/ │ │ │ └── .eleventyignore │ │ ├── ignorelocalrootgitignore/ │ │ │ ├── .eleventyignore │ │ │ └── .gitignore │ │ ├── img/ │ │ │ └── stub.md │ │ ├── included.liquid │ │ ├── includer.liquid │ │ ├── includesemptystring.liquid │ │ ├── index.html │ │ ├── index.liquid │ │ ├── issue-115/ │ │ │ ├── index-with-layout.liquid │ │ │ ├── index.liquid │ │ │ ├── template-bars.liquid │ │ │ └── template-foos.liquid │ │ ├── issue-135/ │ │ │ ├── template.json │ │ │ └── template.njk │ │ ├── issue-522/ │ │ │ ├── excluded.md │ │ │ └── template.md │ │ ├── issue-95/ │ │ │ ├── cat.md │ │ │ └── notacat.md │ │ ├── layout-permalink-difflang/ │ │ │ ├── _includes/ │ │ │ │ └── test.njk │ │ │ └── test.md │ │ ├── layoutsemptystring.liquid │ │ ├── local-data-tags/ │ │ │ ├── component.11tydata.cjs │ │ │ └── component.njk │ │ ├── multiple-ignores/ │ │ │ ├── .eleventyignore │ │ │ ├── ignoredFolder/ │ │ │ │ └── ignored.md │ │ │ └── subfolder/ │ │ │ ├── .eleventyignore │ │ │ └── ignoredFolder2/ │ │ │ └── ignored2.md │ │ ├── multipleexports-promises.11ty.cjs │ │ ├── multipleexports.11ty.cjs │ │ ├── njk-relative/ │ │ │ └── dir/ │ │ │ ├── base.njk │ │ │ ├── imports.njk │ │ │ ├── included.njk │ │ │ └── unique-include-123.njk │ │ ├── object-norender.11ty.cjs │ │ ├── object.11ty.cjs │ │ ├── oneinstance.11ty.cjs │ │ ├── overrides/ │ │ │ ├── layout.njk │ │ │ ├── layoutfalse.njk │ │ │ ├── page-templatesyntax.md │ │ │ ├── test-bypass.md │ │ │ ├── test-empty.html │ │ │ ├── test-empty.md │ │ │ ├── test-error.njk │ │ │ ├── test-md.liquid │ │ │ ├── test-multiple.md │ │ │ ├── test-multiple2.njk │ │ │ ├── test-njk.liquid │ │ │ ├── test.html │ │ │ ├── test.liquid │ │ │ └── test.md │ │ ├── page-target-collections/ │ │ │ ├── paginateall.njk │ │ │ ├── tagpages.njk │ │ │ └── tagpagesall.njk │ │ ├── paged/ │ │ │ ├── cfg-collection-tag-cfg-collection/ │ │ │ │ ├── consumer.njk │ │ │ │ ├── paged-downstream.njk │ │ │ │ ├── paged-main.njk │ │ │ │ ├── test1.njk │ │ │ │ ├── test2.njk │ │ │ │ └── test3.njk │ │ │ ├── collection/ │ │ │ │ ├── consumer.njk │ │ │ │ ├── main.njk │ │ │ │ ├── test1.njk │ │ │ │ ├── test2.njk │ │ │ │ └── test3.njk │ │ │ ├── collection-apply-to-all/ │ │ │ │ ├── consumer.njk │ │ │ │ ├── main.njk │ │ │ │ ├── test1.njk │ │ │ │ ├── test2.njk │ │ │ │ └── test3.njk │ │ │ ├── notpaged.njk │ │ │ ├── paged-before-and-reverse.njk │ │ │ ├── paged-before-filter.njk │ │ │ ├── paged-before-metadata.njk │ │ │ ├── paged-before.njk │ │ │ ├── paged-empty-pageonemptydata.njk │ │ │ ├── paged-empty.njk │ │ │ ├── paged.json │ │ │ ├── paged.njk │ │ │ ├── pagedalias.njk │ │ │ ├── pagedaliassize2.njk │ │ │ ├── pagedinlinedata-reverse.njk │ │ │ ├── pagedinlinedata.njk │ │ │ ├── pagedobject.njk │ │ │ ├── pagedobjectfilterarray.njk │ │ │ ├── pagedobjectfilterstring.njk │ │ │ ├── pagedobjectvalues.njk │ │ │ ├── pagedpermalink.njk │ │ │ ├── pagedpermalinkif.liquid │ │ │ ├── pagedpermalinkif.njk │ │ │ ├── pagedpermalinknumeric.njk │ │ │ ├── pagedpermalinknumericoneindexed.njk │ │ │ └── pagedresolve.njk │ │ ├── paged-global-data-mutable/ │ │ │ ├── _data/ │ │ │ │ └── testdata.cjs │ │ │ └── paged-differing-data-set.njk │ │ ├── pagedate.liquid │ │ ├── pagedate.njk │ │ ├── pagedateutc.njk │ │ ├── pagination-eleventycomputed-permalink.liquid │ │ ├── pagination-eleventycomputed-title.liquid │ │ ├── pagination-templatecontent/ │ │ │ ├── index.njk │ │ │ ├── post-1.md │ │ │ └── post-2.md │ │ ├── permalink-build/ │ │ │ └── permalink-build.md │ │ ├── permalink-conflicts/ │ │ │ ├── test1.md │ │ │ ├── test2.md │ │ │ └── test3.md │ │ ├── permalink-conflicts-false/ │ │ │ ├── test1.md │ │ │ └── test2.md │ │ ├── permalink-data-layout/ │ │ │ ├── test.json │ │ │ └── test.njk │ │ ├── permalink-empty-object/ │ │ │ └── empty-object.md │ │ ├── permalink-false/ │ │ │ └── test.md │ │ ├── permalink-false-computed/ │ │ │ └── test.md │ │ ├── permalink-in-layout-fileslug.liquid │ │ ├── permalink-in-layout.liquid │ │ ├── permalink-markdown-override.md │ │ ├── permalink-markdown-var.md │ │ ├── permalink-markdown.md │ │ ├── permalink-nobuild/ │ │ │ └── permalink-nobuild.md │ │ ├── permalink-true/ │ │ │ └── permalink-true.md │ │ ├── permalinkdata-jsfn.njk │ │ ├── permalinkdata-jspermalinkfn.njk │ │ ├── permalinkdata.njk │ │ ├── permalinkdate.liquid │ │ ├── permalinked.liquid │ │ ├── posts/ │ │ │ ├── post1.njk │ │ │ ├── posts.json │ │ │ └── posts.njk │ │ ├── prematureTemplateContent/ │ │ │ ├── test.11ty.cjs │ │ │ ├── test.liquid │ │ │ ├── test.md │ │ │ └── test.njk │ │ ├── promise.11ty.cjs │ │ ├── public/ │ │ │ └── test.css │ │ ├── relative-liquid/ │ │ │ └── dir/ │ │ │ └── included.liquid │ │ ├── reuse-permalink/ │ │ │ ├── reuse-permalink.json │ │ │ └── test1.liquid │ │ ├── script-frontmatter/ │ │ │ ├── test-default.njk │ │ │ ├── test-js.njk │ │ │ └── test.njk │ │ ├── string.11ty.cjs │ │ ├── string.11ty.custom │ │ ├── string.11ty.possum │ │ ├── stubs-1541/ │ │ │ └── _includes/ │ │ │ └── render-source.liquid │ │ ├── stubs-computed-permalink/ │ │ │ ├── eleventycomputed-nested-object.11ty.cjs │ │ │ ├── eleventycomputed-object-replace.11ty.cjs │ │ │ └── eleventycomputed-object.11ty.cjs │ │ ├── stubs-virtual-conflict/ │ │ │ └── virtual.md │ │ ├── subdir/ │ │ │ ├── img/ │ │ │ │ └── .gitkeep │ │ │ └── index.html │ │ ├── subfolder/ │ │ │ ├── index.html │ │ │ ├── subfolder/ │ │ │ │ └── subfolder.liquid │ │ │ └── subfolder.liquid │ │ ├── tagged-pagination-multiples/ │ │ │ └── test.njk │ │ ├── tagged-pagination-multiples-layout/ │ │ │ └── test.njk │ │ ├── template-passthrough/ │ │ │ ├── .htaccess │ │ │ └── static/ │ │ │ ├── nested/ │ │ │ │ └── test-nested.css │ │ │ ├── test.css │ │ │ └── test.js │ │ ├── template-passthrough2/ │ │ │ ├── .htaccess │ │ │ └── static/ │ │ │ ├── nested/ │ │ │ │ └── test-nested.css │ │ │ ├── test.css │ │ │ └── test.js │ │ ├── template.liquid │ │ ├── templateFrontMatter.liquid │ │ ├── templateFrontMatterJs.njk │ │ ├── templateFrontMatterJson.liquid │ │ ├── templateLayoutCacheDuplicates/ │ │ │ └── _includes/ │ │ │ └── layout.njk │ │ ├── templateLayoutCacheDuplicates-b/ │ │ │ └── _includes/ │ │ │ └── layout.njk │ │ ├── templateMapCollection/ │ │ │ ├── paged-cfg-permalink.md │ │ │ ├── paged-cfg-tagged-apply-to-all.md │ │ │ ├── paged-cfg-tagged-permalink-apply-to-all.md │ │ │ ├── paged-cfg-tagged-permalink.md │ │ │ ├── paged-cfg-tagged.md │ │ │ ├── paged-cfg.md │ │ │ ├── paged-tag-dogs-templateContent-alias.md │ │ │ ├── paged-tag-dogs-templateContent.md │ │ │ ├── paged-tag.md │ │ │ ├── templateContent.md │ │ │ ├── test1.md │ │ │ ├── test2.md │ │ │ ├── test3.md │ │ │ ├── test4.md │ │ │ ├── test5.md │ │ │ └── testWithLayout.md │ │ ├── templateTwoLayouts.liquid │ │ ├── templateWithLayout.liquid │ │ ├── templateWithLayoutContent.liquid │ │ ├── templateWithLayoutKey.liquid │ │ ├── templatetest-frontmatter/ │ │ │ ├── multiple.njk │ │ │ └── single.njk │ │ ├── test-override-js-markdown.11ty.cjs │ │ ├── testing.html │ │ ├── transform-pages/ │ │ │ └── template.njk │ │ ├── use-collection.11ty.cjs │ │ ├── vue-layout.11ty.cjs │ │ ├── vue.11ty.cjs │ │ ├── writeTest/ │ │ │ └── test.md │ │ ├── writeTestJS/ │ │ │ ├── sample.cjs │ │ │ └── test.11ty.cjs │ │ ├── writeTestJS-casesensitive/ │ │ │ ├── sample.Js │ │ │ └── test.11Ty.js │ │ ├── writeTestJS-passthrough/ │ │ │ ├── sample.js │ │ │ └── test.11ty.js │ │ └── writeTestMarkdown/ │ │ ├── sample.md │ │ └── sample2.markdown │ ├── stubs--to/ │ │ ├── test.md │ │ └── test2.liquid │ ├── stubs-1206/ │ │ ├── page1.njk │ │ └── page2.njk │ ├── stubs-1242/ │ │ ├── _data/ │ │ │ ├── xyz.dottest/ │ │ │ │ └── test.json │ │ │ └── xyz.dottest.json │ │ └── empty.md │ ├── stubs-1325/ │ │ ├── test.11ty.js │ │ └── test.js │ ├── stubs-142/ │ │ └── index.njk │ ├── stubs-1691/ │ │ ├── _data/ │ │ │ └── str.txt │ │ ├── template.11tydata.txt │ │ └── template.njk │ ├── stubs-2145/ │ │ ├── _includes/ │ │ │ └── layout.njk │ │ └── test.njk │ ├── stubs-2167/ │ │ └── paginated.njk │ ├── stubs-2224/ │ │ └── index.njk │ ├── stubs-2258/ │ │ ├── _includes/ │ │ │ ├── _code.scss │ │ │ └── layout.njk │ │ ├── eleventy.config.cjs │ │ └── style.scss │ ├── stubs-2258-2830-skip-layouts/ │ │ ├── _includes/ │ │ │ └── layout.njk │ │ ├── eleventy.config.cjs │ │ └── style.scss │ ├── stubs-2261/ │ │ ├── _includes/ │ │ │ └── block.njk │ │ ├── eleventy.config.js │ │ └── index.njk │ ├── stubs-2367/ │ │ ├── _includes/ │ │ │ └── layout.liquid │ │ ├── templateWithLiquidShortcodeMultipleArguments-template2.liquid │ │ └── templateWithLiquidShortcodeMultipleArguments.liquid │ ├── stubs-2602/ │ │ └── index.njk │ ├── stubs-2753/ │ │ ├── _data/ │ │ │ └── global.js │ │ ├── page1.njk │ │ └── page2.njk │ ├── stubs-2790/ │ │ └── page.11ty.cjs │ ├── stubs-2851/ │ │ ├── content.njk │ │ └── paginated.njk │ ├── stubs-3013/ │ │ ├── html/ │ │ │ ├── _data/ │ │ │ │ └── books.json │ │ │ ├── _includes/ │ │ │ │ └── base.html │ │ │ └── book.html │ │ ├── liquid/ │ │ │ ├── _data/ │ │ │ │ └── books.json │ │ │ ├── _includes/ │ │ │ │ └── base.liquid │ │ │ └── book.liquid │ │ ├── md/ │ │ │ ├── _data/ │ │ │ │ └── books.json │ │ │ ├── _includes/ │ │ │ │ └── base.md │ │ │ └── book.md │ │ └── njk/ │ │ ├── _data/ │ │ │ └── books.json │ │ ├── _includes/ │ │ │ └── base.njk │ │ └── book.njk │ ├── stubs-3285/ │ │ └── src/ │ │ └── scripts/ │ │ └── hello-world.js │ ├── stubs-3356/ │ │ └── .gitkeep │ ├── stubs-337/ │ │ ├── data/ │ │ │ └── xyz.json │ │ └── src/ │ │ └── empty.md │ ├── stubs-3807/ │ │ ├── Issue3807test.js │ │ ├── _layouts/ │ │ │ ├── base.html │ │ │ └── home.html │ │ ├── eleventy.config.js │ │ └── index.md │ ├── stubs-3810/ │ │ ├── _includes/ │ │ │ └── promo.njk │ │ ├── eleventy.config.js │ │ └── index.md │ ├── stubs-403/ │ │ ├── .eleventyignore │ │ ├── _includes/ │ │ │ └── include.liquid │ │ └── template.liquid │ ├── stubs-408-sass/ │ │ ├── _code.scss │ │ └── style.scss │ ├── stubs-413/ │ │ └── date-frontmatter.md │ ├── stubs-434/ │ │ └── _includes/ │ │ ├── macros-filter.njk │ │ └── macros.njk │ ├── stubs-475/ │ │ ├── _includes/ │ │ │ └── layout.njk │ │ └── transform-layout/ │ │ └── transform-layout.njk │ ├── stubs-630/ │ │ ├── _data/ │ │ │ ├── globalData0.cjs │ │ │ ├── globalData1.cjs │ │ │ ├── globalData2.json │ │ │ ├── globalData3.yaml │ │ │ ├── globalData4.nosj │ │ │ ├── mergingGlobalData.cjs │ │ │ ├── mergingGlobalData.js │ │ │ ├── mergingGlobalData.json │ │ │ ├── mergingGlobalData.nosj │ │ │ ├── mergingGlobalData.yaml │ │ │ └── subdir/ │ │ │ └── globalDataSubdir.yaml │ │ └── component-yaml/ │ │ ├── component.11tydata.cjs │ │ ├── component.11tydata.json │ │ ├── component.11tydata.nosj │ │ ├── component.11tydata.yaml │ │ ├── component.json │ │ ├── component.njk │ │ └── component.yaml │ ├── stubs-670/ │ │ ├── content.njk │ │ └── index.njk │ ├── stubs-919/ │ │ ├── test.11tydata.cjs │ │ ├── test.njk │ │ └── test2.njk │ ├── stubs-absolute/ │ │ └── test.md │ ├── stubs-addglobaldata/ │ │ └── test.liquid │ ├── stubs-addglobaldata-noop/ │ │ └── test.txt │ ├── stubs-autocopy/ │ │ └── .gitkeep │ ├── stubs-base/ │ │ └── index.njk │ ├── stubs-base-case-sens/ │ │ └── index.njk │ ├── stubs-circular-layout/ │ │ └── _includes/ │ │ ├── layout-cycle-a.njk │ │ ├── layout-cycle-b.njk │ │ ├── layout-cycle-c.njk │ │ └── layout-cycle-self.njk │ ├── stubs-computed-array/ │ │ └── test.liquid │ ├── stubs-computed-collections/ │ │ ├── collections.njk │ │ └── dog.njk │ ├── stubs-computed-collections-filter/ │ │ ├── collections.njk │ │ └── dog.njk │ ├── stubs-computed-dirdata/ │ │ └── dir/ │ │ ├── dir.11tydata.cjs │ │ ├── first.11ty.cjs │ │ └── second.11ty.cjs │ ├── stubs-computed-global/ │ │ ├── _data/ │ │ │ └── eleventyComputed.cjs │ │ └── intermix.njk │ ├── stubs-computed-pagination/ │ │ ├── child.11ty.cjs │ │ └── paginated.njk │ ├── stubs-computed-symbolparse/ │ │ ├── test.liquid │ │ └── test.njk │ ├── stubs-custom-extension/ │ │ └── test.js1 │ ├── stubs-data-cascade/ │ │ ├── global-versus-layout/ │ │ │ ├── _data/ │ │ │ │ └── cascade.cjs │ │ │ ├── _includes/ │ │ │ │ └── base.njk │ │ │ └── test.njk │ │ ├── layout-data-files/ │ │ │ ├── _includes/ │ │ │ │ └── base.njk │ │ │ ├── test.11tydata.cjs │ │ │ └── test.njk │ │ ├── layout-versus-dirdatafile/ │ │ │ └── src/ │ │ │ ├── _includes/ │ │ │ │ └── base.njk │ │ │ ├── src.11tydata.cjs │ │ │ └── test.njk │ │ └── layout-versus-tmpldatafile/ │ │ ├── _includes/ │ │ │ └── base.njk │ │ ├── test.11tydata.cjs │ │ └── test.njk │ ├── stubs-data-esm/ │ │ └── _data/ │ │ ├── commonjs.cjs │ │ └── module.mjs │ ├── stubs-dependency-tree/ │ │ ├── child.cjs │ │ ├── grandchild.cjs │ │ └── index.cjs │ ├── stubs-empty/ │ │ └── .gitkeep │ ├── stubs-empty-json-data/ │ │ └── _data/ │ │ └── empty.json │ ├── stubs-fancyjs/ │ │ ├── test.11ty.tsx │ │ └── test.mdx │ ├── stubs-freeze/ │ │ ├── eleventy/ │ │ │ └── _data/ │ │ │ └── eleventy.js │ │ └── page/ │ │ └── _data/ │ │ └── page.js │ ├── stubs-global-data-config-api/ │ │ └── empty.txt │ ├── stubs-global-data-config-api-nested/ │ │ └── _data/ │ │ └── deep.cjs │ ├── stubs-i18n/ │ │ ├── en/ │ │ │ └── index.liquid │ │ ├── en-us/ │ │ │ └── index.11ty.cjs │ │ ├── es/ │ │ │ └── index.njk │ │ └── non-lang-file.njk │ ├── stubs-img-transform/ │ │ ├── ignored.md │ │ ├── missing-alt.md │ │ ├── multiple.md │ │ └── single.md │ ├── stubs-incremental/ │ │ └── layout-chain/ │ │ ├── _includes/ │ │ │ ├── base.njk │ │ │ └── parent.njk │ │ └── test.njk │ ├── stubs-layout-cache/ │ │ ├── _includes/ │ │ │ ├── layout.liquid │ │ │ └── layout.njk │ │ ├── test.liquid │ │ └── test.njk │ ├── stubs-layouts-event/ │ │ ├── _includes/ │ │ │ ├── first.liquid │ │ │ ├── second.liquid │ │ │ └── third.liquid │ │ └── page.md │ ├── stubs-njk-async/ │ │ └── _includes/ │ │ └── loop.njk │ ├── stubs-pagination-computed-quotes/ │ │ ├── post.liquid │ │ └── test.liquid │ ├── stubs-pagination-computed-quotes-njk/ │ │ ├── post.njk │ │ └── test.njk │ ├── stubs-pathtourl/ │ │ ├── css.njk │ │ ├── filter.njk │ │ ├── tmpl.njk │ │ └── transform.njk │ ├── stubs-render-plugin/ │ │ ├── 11tyjs-file-override.njk │ │ ├── 11tyjs-file.njk │ │ ├── 11tyjs.liquid │ │ ├── _includes/ │ │ │ ├── frontmatter.liquid │ │ │ ├── include-js.txt │ │ │ ├── include.11ty.cjs │ │ │ ├── include.liquid │ │ │ └── include.njk │ │ ├── bad-data.njk │ │ ├── capture-liquid.njk │ │ ├── capture-njk.liquid │ │ ├── data-no-templatelang.liquid │ │ ├── false.liquid │ │ ├── liquid-direct.njk │ │ ├── liquid-eleventy.njk │ │ ├── liquid-global.njk │ │ ├── liquid-md.11ty.cjs │ │ ├── liquid-md.liquid │ │ ├── liquid-page.liquid │ │ ├── liquid-page.njk │ │ ├── liquid.njk │ │ ├── md.liquid │ │ ├── njk-eleventy.liquid │ │ ├── njk-file-not-exist.liquid │ │ ├── njk-file.liquid │ │ ├── njk-file.njk │ │ ├── njk-page.liquid │ │ ├── nunjucks-frontmatter.njk │ │ ├── nunjucks-global.liquid │ │ ├── nunjucks.11ty.cjs │ │ ├── nunjucks.liquid │ │ ├── using-frontmatter.liquid │ │ └── vue.liquid │ ├── stubs-virtual/ │ │ ├── .gitkeep │ │ └── eleventy.config.js │ ├── stubs-virtual-nowrite/ │ │ └── .gitkeep │ └── views/ │ └── .gitkeep ├── test_node/ │ ├── 3824/ │ │ ├── 3824-test.js │ │ ├── _includes/ │ │ │ ├── head.tsx │ │ │ └── view-props.tsx │ │ ├── eleventy.config.js │ │ ├── index.11ty.tsx │ │ └── tsconfig-3824.json │ ├── 3824-incremental/ │ │ ├── 3824-incremental-test.js │ │ ├── _includes/ │ │ │ ├── head.tsx │ │ │ └── view-props.tsx │ │ ├── eleventy.config.js │ │ ├── index.11ty.tsx │ │ └── tsconfig-3824.json │ ├── JsxTest.js │ ├── MdxTest.js │ ├── README.md │ └── tests.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 [*.yml] indent_style = space [/test/**/*] indent_style = space [/test/stubs*/**] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .git-blame-ignore-revs ================================================ # Switch the entire codebase to tabs, for accessibility #3098 358ec48f779fa34e14abef057cc1fa0c1a10aa45 ================================================ FILE: .github/CODEOWNERS ================================================ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners * @zachleat ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: I have a question about Eleventy url: https://github.com/11ty/eleventy/discussions/ about: General education topics should be filed on our Discussions board e.g. “How do I do this in Eleventy?” or “Can Eleventy do this?” (Please search existing discussions first!) - name: I have a feature request for Eleventy url: https://github.com/11ty/eleventy/discussions/new?category=enhancement-queue about: Enhancement or new Features. e.g. “I wish Eleventy did this.” Suggest an idea! (Please search existing discussions first!) - name: I wish the docs were different! url: https://github.com/11ty/11ty-website/issues/new/choose about: Something missing from the documentation? Something wrong? Something confusing? You want the 11ty-website repo! - name: Discord Community url: https://discord.gg/GBkBy9u about: Ask the community on Discord ================================================ FILE: .github/ISSUE_TEMPLATE/possible-bug.yml ================================================ name: I’m having trouble with Eleventy description: Have a problem? It might be a bug! Create a report to help us improve. labels: [needs-triage] body: - type: markdown attributes: value: | Before opening a bug report, please search for the behavior in the existing issues. --- Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information. - type: input id: os attributes: label: Operating system description: Which operating system do you use? placeholder: macOS Big Sur 11.5.2 validations: required: true - type: input id: eleventy attributes: label: Eleventy description: Which version of Eleventy do you use? placeholder: eleventy --version or npx @11ty/eleventy --version validations: required: true - type: textarea id: bug-description attributes: label: Describe the bug description: A clear and concise description of how to reproduce the bug value: | 1. I ran *this* command with *these* flags '...' 2. I used the following configuration and template syntax '....' 3. I got *this* error or I expected this to happen and it didn’t validations: required: true - type: input id: repro-url attributes: label: Reproduction Source Code URL description: "Optional: The URL to the **public** repository for the reproduction. _[parser:url]_" placeholder: e.g. https://github.com/zachleat/zachleat.com validations: required: false - type: textarea id: screenshots attributes: label: Screenshots description: "Optional: If applicable, add screenshots to help explain your problem." ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: weekly cooldown: default-days: 7 labels: - "dependency-updates" versioning-strategy: increase allow: - dependency-type: "production" assignees: [zachleat] - package-ecosystem: github-actions directories: [".github/workflows/**"] schedule: interval: weekly cooldown: default-days: 7 assignees: [zachleat] ================================================ FILE: .github/opencollective.yml ================================================ collective: 11ty tiers: - tiers: '*' labels: ["oc-supporter"] message: "Hey , thanks for supporting us on Open Collective!" invitation: | Hey :wave:, Thank you for opening an issue. We will get back to you as soon as we can. Also, check out our [Open Collective](https://opencollective.com/11ty) and consider backing us—every little bit helps! ================================================ FILE: .github/workflows/ci.yml ================================================ name: Unit Tests on: push permissions: read-all jobs: server: name: Node.js v${{ matrix.node }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] node: ["20", "22", "24"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - name: Setup node uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # 6.2.0 with: node-version: ${{ matrix.node }} # cache: npm - run: npm ci - run: npm run test:server client: name: Vitest on ${{ matrix.os }} (Node.js v${{ matrix.node }}) runs-on: ${{ matrix.os }} strategy: matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] node: ["22"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - name: Setup node uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # 6.2.0 with: node-version: ${{ matrix.node }} - run: npm ci - run: npx playwright install - run: npm run test:client env: YARN_GPG: no ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL Advanced" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '21 17 * * 5' # Declare default permissions as read only. permissions: read-all jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: javascript-typescript build-mode: none # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 #4.31.9 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 #4.31.9 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish Release to npm on: release: types: [published] permissions: read-all jobs: release: # see https://github.com/11ty/eleventy/settings/environments environment: GitHub Publish runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # 6.2.0 with: node-version: "22" registry-url: 'https://registry.npmjs.org' - run: npm install -g npm@latest - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} # Also runs npm ci and npm test run: ./scripts/release.sh env: NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-beta.') && 'beta' || contains(github.event.release.tag_name, '-alpha.') && 'canary' || 'latest' }} ================================================ FILE: .github/workflows/scorecard.yml ================================================ # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '35 7 * * 2' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. # - name: "Upload artifact" # uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 # with: # name: SARIF file # path: results.sarif # retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard # - name: "Upload to code-scanning" # uses: github/codeql-action/upload-sarif@v3 # with: # sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ # Generated files dist/ packages/*/visualize/* # Ignore installed npm modules node_modules/ # Ignore build tool output, e.g. code coverage .nyc_output/ coverage/ docs/_data/coverage.json # Ignore API documentation api-docs/ # Ignore folders from source code editors .vscode .idea # Ignore eleventy output when doing manual tests _site/ # Ignore test files .cache test/stubs-layout-cache/_includes/*.js ================================================ FILE: .npmignore ================================================ docs docs-src test test_node coverage eslint.config.js .* packages/client/ ================================================ FILE: .nvmrc ================================================ 22 ================================================ FILE: .prettierignore ================================================ test ================================================ FILE: .prettierrc.json ================================================ { "useTabs": true, "singleQuote": false, "semi": true, "endOfLine": "lf", "arrowParens": "always", "printWidth": 100 } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Eleventy Community Code of Conduct View the [Code of Conduct](https://www.11ty.dev/docs/code-of-conduct/) on 11ty.dev ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, chat messages, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eleventy@zachleat.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017–2024 Zach Leatherman @zachleat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

eleventy Logo

# eleventy 🕚⚡️🎈🐀 A simpler static site generator. An alternative to Jekyll. Written in JavaScript. Transforms a directory of templates (of varying types) into HTML. Works with HTML, Markdown, JavaScript, Liquid, Nunjucks, with addons for WebC, Sass, Vue, Svelte, TypeScript, JSX, and many others! ## ➡ [Documentation](https://www.11ty.dev/docs/) - Star [this repo on GitHub](https://github.com/11ty/eleventy/)! - Follow us [on Mastodon `@11ty@neighborhood.11ty.dev`](https://neighborhood.11ty.dev/@11ty) - Follow us [on Bluesky `@11ty.dev`](https://bsky.app/profile/11ty.dev) - Install [from npm](https://www.npmjs.com/org/11ty) - Follow [on GitHub](https://github.com/11ty) - Watch us [on YouTube](https://www.youtube.com/c/EleventyVideo) - Chat on [Discord](https://www.11ty.dev/blog/discord/) - Latest: [![npm Version](https://img.shields.io/npm/v/@11ty/eleventy.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/eleventy) ## Installation ``` npm install @11ty/eleventy --save-dev ``` Read our [Getting Started guide](https://www.11ty.dev/docs/getting-started/). ## Tests ``` npm test ``` We have a few test suites, for various reasons: - [ava JavaScript test runner](https://github.com/avajs/ava) ([assertions docs](https://github.com/avajs/ava/blob/main/docs/03-assertions.md)) (primary test suite in `test/`) - [Node.js Test runner](https://nodejs.org/api/test.html) (secondary test suite in `test_node/`) - [Vitest (in Browser Mode)](https://vitest.dev/guide/browser/) (client tests in `packages/client/test/`) - [Benchmark for Performance Regressions](https://github.com/11ty/eleventy-benchmark) These run in various environments: - [Continuous Integration on GitHub Actions](https://github.com/11ty/eleventy/actions/workflows/ci.yml) - [Code Coverage Statistics](https://github.com/11ty/eleventy/blob/master/docs/coverage.md) ## Community Roadmap - [Top Feature Requests](https://github.com/11ty/eleventy/discussions/categories/enhancement-queue?discussions_q=is%3Aopen+category%3A%22Enhancement+Queue%22+sort%3Atop) (Vote for your favorites!) - [Top Bugs 😱](https://github.com/11ty/eleventy/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions) (Add your own votes using the 👍 reaction) ## Plugins See the [official docs on plugins](https://www.11ty.dev/docs/plugins/). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Privately report a security issue by navigating to https://github.com/11ty/eleventy/security and using the “Report a vulnerability” button. Read more at: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability Alternatively, you may report security issues via an email to `security@11ty.dev`. ================================================ FILE: cmd.cjs ================================================ #!/usr/bin/env node // This file intentionally uses older code conventions to be as friendly // as possible with error messaging to folks on older runtimes. const pkg = require("./package.json"); require("@11ty/node-version-check")(pkg, { message: function (requiredVersion) { return ( "Eleventy " + pkg.version + " requires Node " + requiredVersion + ". You will need to upgrade Node to use Eleventy!" ); }, }); const minimist = require("minimist"); const debug = require("debug")("Eleventy:cmd"); class SimpleError extends Error { constructor(...args) { super(...args); this.skipOriginalStack = true; } } async function exec() { // Notes about friendly error messaging with outdated Node versions: https://github.com/11ty/eleventy/issues/3761 const { EleventyErrorHandler } = await import("./src/Errors/EleventyErrorHandler.js"); // Defensive use of Node 22.8+ Module Compile Cache if(!process.env?.ELEVENTY_SKIP_NODE_COMPILE_CACHE) { try { const nodeMod = await import('node:module').then(mod => mod.default); nodeMod.enableCompileCache?.(); } catch(e) { debug("Node compile cache error (optional API) %o", e); } } try { const argv = minimist(process.argv.slice(2), { string: ["input", "output", "formats", "config", "pathprefix", "port", "to", "incremental", "loader"], boolean: [ "quiet", "version", "watch", "dryrun", "help", "serve", "ignore-initial", ], default: { quiet: null, "ignore-initial": false, "to": "fs", }, unknown: function (unknownArgument) { throw new Error( `We don’t know what '${unknownArgument}' is. Use --help to see the list of supported commands.`, ); }, }); debug("command: eleventy %o", argv); const { Eleventy } = await import("./src/Eleventy.js"); let ErrorHandler = new EleventyErrorHandler(); process.on("unhandledRejection", (error, promise) => { ErrorHandler.fatal(error, "Unhandled rejection in promise"); }); process.on("uncaughtException", (error) => { ErrorHandler.fatal(error, "Uncaught exception"); }); process.on("rejectionHandled", (promise) => { ErrorHandler.warn(promise, "A promise rejection was handled asynchronously"); }); if (argv.version) { console.log(Eleventy.getVersion()); return; } else if (argv.help) { console.log(Eleventy.getHelp()); return; } let elev = new Eleventy(argv.input, argv.output, { source: "cli", // --quiet and --quiet=true both resolve to true quietMode: argv.quiet, configPath: argv.config, pathPrefix: argv.pathprefix, runMode: argv.serve ? "serve" : argv.watch ? "watch" : "build", dryRun: argv.dryrun, loader: argv.loader, }); // reuse ErrorHandler instance in Eleventy ErrorHandler = elev.errorHandler; // Before init elev.setFormats(argv.formats); await elev.init(); if (argv.to === "json") { // override logging output elev.setIsVerbose(false); } // Only relevant for watch/serve elev.setIgnoreInitial(argv["ignore-initial"]); if(argv.incremental) { elev.setIncrementalFile(argv.incremental); } else if(argv.incremental !== undefined) { elev.setIncrementalBuild(argv.incremental === "" || argv.incremental); } if (argv.serve || argv.watch) { if(argv.to === "json") { throw new SimpleError("--to=json is not compatible with --serve or --watch."); } await elev.watch(); if (argv.serve) { // TODO await here? elev.serve(argv.port); } process.on("SIGINT", async () => { await elev.stopWatch(); process.exitCode = 0; }); } else { // `fs:templates` will skip passthrough copy if (!argv.to || argv.to === "fs" || argv.to.startsWith("fs:")) { await elev.write(argv.to); } else if (argv.to === "json") { let result = await elev.toJSON() console.log(JSON.stringify(result, null, 2)); } else { throw new SimpleError( `Invalid --to value: ${argv.to}. Supported values: \`fs\` (default), \`json\`.`, ); } } } catch (error) { if(typeof EleventyErrorHandler !== "undefined") { let ErrorHandler = new EleventyErrorHandler(); ErrorHandler.fatal(error, "Eleventy Fatal Error (CLI)"); } else { console.error(error); process.exitCode = 1; } } } // await exec(); ================================================ FILE: docs/coverage.md ================================================ # Code Coverage for Eleventy v3.1.2 | Filename | % Lines | % Statements | % Functions | % Branches | | ---------------------------------------------------------- | ------- | ------------ | ----------- | ---------- | | `total` | 90.67% | 90.67% | 90.51% | 89.18% | | `cmd.cjs` | 71.61% | 71.61% | 25% | 68.18% | | `src/Eleventy.js` | 91.11% | 91.11% | 87.09% | 84.15% | | `src/EleventyExtensionMap.js` | 97.18% | 97.18% | 96.15% | 94.31% | | `src/EleventyFiles.js` | 95.39% | 95.39% | 95.74% | 90.99% | | `src/EleventyServe.js` | 52.33% | 52.33% | 69.56% | 58.33% | | `src/EleventyWatch.js` | 93.12% | 93.12% | 94.44% | 91.66% | | `src/EleventyWatchTargets.js` | 93.29% | 93.29% | 90% | 90.47% | | `src/EventBus.js` | 100% | 100% | 100% | 100% | | `src/FileSystemSearch.js` | 100% | 100% | 100% | 100% | | `src/GlobalDependencyMap.js` | 81.85% | 81.85% | 85% | 88.23% | | `src/LayoutCache.js` | 77.55% | 77.55% | 87.5% | 87.5% | | `src/Template.js` | 94.91% | 94.91% | 93.84% | 90.73% | | `src/TemplateBehavior.js` | 90.58% | 90.58% | 100% | 84.21% | | `src/TemplateCollection.js` | 94.8% | 94.8% | 87.5% | 95.23% | | `src/TemplateConfig.js` | 91.85% | 91.85% | 82.35% | 92.66% | | `src/TemplateContent.js` | 90.24% | 90.24% | 91.83% | 87.02% | | `src/TemplateFileSlug.js` | 100% | 100% | 100% | 100% | | `src/TemplateGlob.js` | 94.28% | 94.28% | 100% | 91.66% | | `src/TemplateLayout.js` | 90.83% | 90.83% | 83.33% | 88.57% | | `src/TemplateLayoutPathResolver.js` | 88.97% | 88.97% | 75% | 90.9% | | `src/TemplateMap.js` | 95.17% | 95.17% | 94.87% | 94.65% | | `src/TemplatePassthrough.js` | 92.8% | 92.8% | 100% | 90% | | `src/TemplatePassthroughManager.js` | 86.95% | 86.95% | 96.29% | 83.52% | | `src/TemplatePermalink.js` | 87.69% | 87.69% | 91.66% | 92.95% | | `src/TemplateRender.js` | 90.06% | 90.06% | 100% | 91.5% | | `src/TemplateWriter.js` | 84.64% | 84.64% | 83.33% | 74.69% | | `src/UserConfig.js` | 90.96% | 90.96% | 78.57% | 87.36% | | `src/defaultConfig.js` | 96.06% | 96.06% | 100% | 66.66% | | `src/Benchmark/Benchmark.js` | 98.18% | 98.18% | 100% | 92.3% | | `src/Benchmark/BenchmarkGroup.js` | 92.59% | 92.59% | 81.81% | 95.45% | | `src/Benchmark/BenchmarkManager.js` | 90.41% | 90.41% | 77.77% | 87.5% | | `src/Data/ComputedData.js` | 100% | 100% | 100% | 100% | | `src/Data/ComputedDataProxy.js` | 97.7% | 97.7% | 100% | 94.44% | | `src/Data/ComputedDataQueue.js` | 100% | 100% | 100% | 100% | | `src/Data/ComputedDataTemplateString.js` | 95.71% | 95.71% | 100% | 85.71% | | `src/Data/TemplateData.js` | 93.09% | 93.09% | 94.11% | 88.12% | | `src/Data/TemplateDataInitialGlobalData.js` | 95% | 95% | 100% | 83.33% | | `src/Engines/Custom.js` | 88.2% | 88.2% | 100% | 86.59% | | `src/Engines/Html.js` | 100% | 100% | 100% | 100% | | `src/Engines/JavaScript.js` | 81.25% | 81.25% | 93.75% | 86.44% | | `src/Engines/Liquid.js` | 99.09% | 99.09% | 100% | 95.94% | | `src/Engines/Markdown.js` | 96% | 96% | 85.71% | 92.59% | | `src/Engines/Nunjucks.js` | 92.32% | 92.32% | 100% | 87.5% | | `src/Engines/TemplateEngine.js` | 89.32% | 89.32% | 83.87% | 92.68% | | `src/Engines/TemplateEngineManager.js` | 93.78% | 93.78% | 100% | 92.64% | | `src/Engines/FrontMatter/JavaScript.js` | 100% | 100% | 100% | 100% | | `src/Engines/Util/ContextAugmenter.js` | 91.04% | 91.04% | 50% | 88.23% | | `src/Errors/DuplicatePermalinkOutputError.js` | 100% | 100% | 100% | 100% | | `src/Errors/EleventyBaseError.js` | 100% | 100% | 100% | 100% | | `src/Errors/EleventyErrorHandler.js` | 94.07% | 94.07% | 100% | 77.77% | | `src/Errors/EleventyErrorUtil.js` | 100% | 100% | 100% | 100% | | `src/Errors/TemplateContentPrematureUseError.js` | 100% | 100% | 100% | 100% | | `src/Errors/TemplateContentUnrenderedTemplateError.js` | 100% | 100% | 100% | 100% | | `src/Errors/UsingCircularTemplateContentReferenceError.js` | 100% | 100% | 100% | 100% | | `src/Filters/GetCollectionItem.js` | 100% | 100% | 100% | 87.5% | | `src/Filters/GetCollectionItemIndex.js` | 88.23% | 88.23% | 100% | 77.77% | | `src/Filters/GetLocaleCollectionItem.js` | 12.76% | 12.76% | 0% | 100% | | `src/Filters/Slug.js` | 100% | 100% | 100% | 100% | | `src/Filters/Slugify.js` | 100% | 100% | 100% | 100% | | `src/Filters/Url.js` | 88.57% | 88.57% | 100% | 93.75% | | `src/Plugins/HtmlBasePlugin.js` | 85% | 85% | 100% | 86.95% | | `src/Plugins/HtmlRelativeCopyPlugin.js` | 100% | 100% | 100% | 100% | | `src/Plugins/I18nPlugin.js` | 82.96% | 82.96% | 100% | 80.55% | | `src/Plugins/IdAttributePlugin.js` | 97.27% | 97.27% | 100% | 90% | | `src/Plugins/InputPathToUrl.js` | 90.05% | 90.05% | 100% | 78.12% | | `src/Plugins/Pagination.js` | 90.23% | 90.23% | 95% | 84% | | `src/Plugins/RenderPlugin.js` | 87.69% | 87.69% | 86.36% | 77.77% | | `src/Util/ArrayUtil.js` | 100% | 100% | 100% | 100% | | `src/Util/AsyncEventEmitter.js` | 95.45% | 95.45% | 100% | 89.47% | | `src/Util/Compatibility.js` | 79.66% | 79.66% | 75% | 77.77% | | `src/Util/ConsoleLogger.js` | 100% | 100% | 95% | 100% | | `src/Util/DateGitFirstAdded.js` | 100% | 100% | 100% | 100% | | `src/Util/DateGitLastUpdated.js` | 100% | 100% | 100% | 100% | | `src/Util/DirContains.js` | 100% | 100% | 100% | 100% | | `src/Util/EsmResolver.js` | 84.9% | 84.9% | 100% | 85.71% | | `src/Util/ExistsCache.js` | 96.77% | 96.77% | 85.71% | 100% | | `src/Util/FilePathUtil.js` | 47.36% | 47.36% | 50% | 100% | | `src/Util/FileSystemManager.js` | 72.91% | 72.91% | 66.66% | 87.5% | | `src/Util/GetJavaScriptData.js` | 100% | 100% | 100% | 100% | | `src/Util/GlobMatcher.js` | 90.9% | 90.9% | 100% | 66.66% | | `src/Util/GlobRemap.js` | 97.64% | 97.64% | 90% | 100% | | `src/Util/HtmlRelativeCopy.js` | 90.6% | 90.6% | 100% | 89.18% | | `src/Util/HtmlTransformer.js` | 90.11% | 90.11% | 88.88% | 90.69% | | `src/Util/ImportJsonSync.js` | 84.41% | 84.41% | 83.33% | 92.3% | | `src/Util/IsAsyncFunction.js` | 100% | 100% | 50% | 100% | | `src/Util/JavaScriptDependencies.js` | 89.09% | 89.09% | 50% | 85.71% | | `src/Util/MemoizeFunction.js` | 100% | 100% | 100% | 100% | | `src/Util/PassthroughCopyBehaviorCheck.js` | 100% | 100% | 100% | 100% | | `src/Util/PathNormalizer.js` | 93.1% | 93.1% | 100% | 86.66% | | `src/Util/PathPrefixer.js` | 100% | 100% | 100% | 100% | | `src/Util/Pluralize.js` | 100% | 100% | 100% | 100% | | `src/Util/ProjectDirectories.js` | 96.74% | 96.74% | 97.29% | 96.11% | | `src/Util/ProjectTemplateFormats.js` | 94.02% | 94.02% | 90% | 94.73% | | `src/Util/PromiseUtil.js` | 46.66% | 46.66% | 100% | 66.66% | | `src/Util/Require.js` | 82.94% | 82.94% | 75% | 82.05% | | `src/Util/ReservedData.js` | 97.1% | 97.1% | 100% | 92.85% | | `src/Util/SetUnion.js` | 100% | 100% | 100% | 100% | | `src/Util/SpawnAsync.js` | 96.55% | 96.55% | 100% | 87.5% | | `src/Util/TemplateDepGraph.js` | 96.25% | 96.25% | 100% | 93.61% | | `src/Util/TransformsUtil.js` | 94.28% | 94.28% | 100% | 83.33% | | `src/Util/ValidUrl.js` | 100% | 100% | 100% | 100% | | `src/Util/Objects/DeepFreeze.js` | 90% | 90% | 100% | 80% | | `src/Util/Objects/ObjectFilter.js` | 100% | 100% | 100% | 80% | | `src/Util/Objects/ProxyWrap.js` | 96.61% | 96.61% | 100% | 94.73% | | `src/Util/Objects/Sortable.js` | 100% | 100% | 100% | 100% | | `src/Util/Objects/Unique.js` | 100% | 100% | 100% | 100% | ================================================ FILE: docs/coverage.njk ================================================ --- permalink: coverage.md --- # Code Coverage for Eleventy v{{ pkg.version }} | Filename | % Lines | % Statements | % Functions | % Branches | | --- | --- | --- | --- | --- | {% for file, line in coverage -%} | `{{ file | removeDir }}` | {{ line.lines.pct }}% | {{ line.statements.pct }}% | {{ line.functions.pct }}% | {{ line.branches.pct }}% | {% endfor -%} ================================================ FILE: docs/eleventy.coverage.js ================================================ import { dirname } from "path"; import { fileURLToPath } from "url"; import { TemplatePath } from "@11ty/eleventy-utils"; const __dirname = dirname(fileURLToPath(import.meta.url)); export default function (eleventyConfig) { eleventyConfig.addFilter("removeDir", function (str) { return TemplatePath.stripLeadingSubPath(str, TemplatePath.join(__dirname, "..")); }); return { templateFormats: ["njk"], dir: { input: "docs/coverage.njk", output: "docs/", // root relative }, }; } ================================================ FILE: docs/release-instructions.md ================================================ # Dependency notes - (dev dep only) `@iarna/toml` has a 3.0 that we have never been on but it was released the same day as the last 2.x https://github.com/BinaryMuse/toml-node/commits/master (needs more investigation) # Release Procedure 1. (Optional) Update minor dependencies in package.json - `npx npm-check-updates --target minor -u --dep prod` - or `npm outdated` + `npm update --save` 1. Stable release only: make sure there aren’t any `@11ty/*` dependencies on pre-release versions alpha/beta/canary 1. If the minimum Node version changed, make sure you update `package.json` engines property. - Make sure the error message works correctly for Node versions less than 10. - 0.12.x+ requires Node 10+ - 1.x+ requires Node 12+ - 2.x+ requires Node 14+ - 3.x+ requires Node 18+ - 4.x+ requires Node 20+ 1. `rm -rf node_modules && rm -f package-lock.json && npm install` 1. `npm audit --omit=dev` 1. Make sure `npm run check` (eslint) runs okay 1. Make sure `npm run test` (ava) runs okay 1. Update version in `package.json` - (Alpha) Use `-alpha.1` suffix - (Beta) Use `-beta.1` suffix 1. Run `npm run coverage` 1. Check it all in and commit 1. Tag new version 1. Wait for GitHub Actions to complete to know that the build did not fail. 1. Publish a release on GitHub at https://github.com/11ty/eleventy/releases pointing to the tag of the release. Hitting the publish button on this workflow will use GitHub Actions to publish the package to npm on the correct dist-tag and includes npm package provenance for the release. - Main release: no version suffix publishes to `latest` (default) tag on npm - Make sure to include OpenCollective usernames for release notes here https://www.11ty.dev/supporters-for-release-notes/ - Canary release: `-alpha.` version suffix in `package.json` publishes to `canary` tag on npm: https://github.com/11ty/eleventy/issues/2758 - Beta release: `-beta.` version suffix publishes to `beta` tag on npm Unfortunate note about npm and tags (specifically `canary` here): if you push a 1.0.0-canary.x to `canary` (even though `2.0.0-canary.x` exists), it will use the last pushed tag when you npm install from `@canary` (not the highest version number) # Docs/Website (Main releases only) 1. Maybe search for `-alpha.` (`-canary.`?) or `-beta.` in the docs copy to update to the stable release, if applicable. 1. Check in a new `11ty-website` site with updated `package.json` version. 1. Add version to `11ty-website` `versions.json` 1. Commit it 1. Create a new branch for branched version 1. (Main) Check out the previous version git branch and add `outdated: true` to `_data/config.json` and commit/push. 1. Go to https://app.netlify.com/sites/11ty/settings/domain and set up a subdomain for it. # Downstream dependencies 1. Update `eleventy-base-blog` to use new version 1. Update `11ty-website` to use new version ================================================ FILE: eslint.config.js ================================================ import globals from "globals"; import pluginJs from "@eslint/js"; import stylisticJs from "@stylistic/eslint-plugin-js"; import prettier from "eslint-config-prettier"; export const GLOB_SRC_EXT = "?([cm])[jt]s?(x)"; export const GLOB_TESTS = [ `**/test/**/*.${GLOB_SRC_EXT}`, `**/__tests__/**/*.${GLOB_SRC_EXT}`, `**/*.spec.${GLOB_SRC_EXT}`, `**/*.test.${GLOB_SRC_EXT}`, `**/*.bench.${GLOB_SRC_EXT}`, `**/*.benchmark.${GLOB_SRC_EXT}`, ]; export default [ { name: "11ty/setup/js", ...pluginJs.configs.recommended, }, { name: "11ty/rules/project-specific", plugins: { "@stylistic/js": stylisticJs, }, languageOptions: { globals: { ...globals.node, }, ecmaVersion: "latest", sourceType: "module", }, rules: { "no-async-promise-executor": "warn", "no-prototype-builtins": "warn", "no-unused-vars": "warn", "@stylistic/js/space-unary-ops": "error", }, }, { name: "11ty/ignores", files: GLOB_TESTS, rules: { "no-unused-vars": "off", }, }, { name: "11ty/setup/prettier", ...prettier, }, ]; ================================================ FILE: package.json ================================================ { "name": "@11ty/eleventy", "version": "4.0.0-alpha.6", "description": "A simpler static site generator.", "publishConfig": { "access": "public", "provenance": true }, "type": "module", "main": "./src/Eleventy.js", "exports": { ".": { "import": "./src/Eleventy.js", "require": "./src/EleventyCommonJs.cjs" }, "./UserConfig": { "types": "./src/UserConfig.js" }, "./utils/git": "./src/Util/Git.js" }, "bin": { "eleventy": "cmd.cjs" }, "license": "MIT", "engines": { "node": ">=20.19" }, "workspaces": [ "packages/client" ], "funding": { "type": "opencollective", "url": "https://opencollective.com/11ty" }, "keywords": [ "static-site-generator", "static-site", "ssg", "documentation", "website", "jekyll", "blog", "templates", "generator", "framework", "eleventy", "11ty", "html", "markdown", "liquid", "nunjucks" ], "scripts": { "default": "npm run test", "test": "npm run test:server && npm run test:client", "test:server": "npm run test:node && npm run test:ava", "test:ava": "ava --verbose --timeout 20s", "test:node": "node --test test_node/tests.js", "test:client": "cross-env CI=true npm run test --workspaces", "format": "prettier . --write", "check": "eslint src", "check-types": "tsc", "nano-staged": "nano-staged", "coverage": "npx c8 ava && npx c8 report --reporter=json-summary && cp coverage/coverage-summary.json docs/_data/coverage.json && node cmd.cjs --config=docs/eleventy.coverage.js", "prepare": "simple-git-hooks" }, "author": "Zach Leatherman (https://zachleat.com/)", "repository": { "type": "git", "url": "git+https://github.com/11ty/eleventy.git" }, "bugs": "https://github.com/11ty/eleventy/issues", "homepage": "https://www.11ty.dev/", "ava": { "environmentVariables": {}, "failFast": true, "files": [ "./test/*.js", "./test/_issues/**/*test.js" ], "watchMode": { "ignoreChanges": [ "./test/stubs*/**/*", "./test/**/_site/**/*", ".cache" ] } }, "nano-staged": { "*.{js,css,md}": [ "prettier --write" ] }, "simple-git-hooks": { "pre-commit": "npm test && npm run nano-staged", "pre-push": "npm test" }, "devDependencies": { "@11ty/eleventy-img": "^6.0.4", "@11ty/eleventy-plugin-rss": "^2.0.4", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "@11ty/eleventy-plugin-webc": "^0.12.0-beta.7", "@eslint/js": "^10.0.1", "@iarna/toml": "^2.2.5", "@mdx-js/node-loader": "^3.1.1", "@stylistic/eslint-plugin-js": "^4.4.1", "@types/node": "^25.5.0", "@vue/server-renderer": "^3.5.30", "@zachleat/noop": "^1.0.7", "ava": "^6.4.1", "c8": "^10.1.3", "cross-env": "^10.1.0", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", "jsx-async-runtime": "^2.0.3", "luxon": "^3.7.2", "markdown-it-abbr": "^2.0.0", "markdown-it-emoji": "^3.0.0", "marked": "^17.0.4", "nano-staged": "^0.9.0", "prettier": "^3.8.1", "pretty": "^2.0.0", "react": "^19.2.4", "react-dom": "^19.2.4", "sass": "^1.98.0", "simple-git-hooks": "^2.13.1", "tsx": "^4.21.0", "typescript": "^5.9.3", "vue": "^3.5.30", "zod": "^4.3.6", "zod-validation-error": "^5.0.0" }, "dependencies": { "@11ty/dependency-tree": "^4.0.2", "@11ty/dependency-tree-esm": "^2.0.4", "@11ty/dependency-tree-typescript": "^1.0.0", "@11ty/eleventy-dev-server": "^3.0.0-alpha.6", "@11ty/eleventy-plugin-bundle": "^3.0.7", "@11ty/eleventy-utils": "^2.0.7", "@11ty/gray-matter": "^2.0.1", "@11ty/lodash-custom": "^4.17.21", "@11ty/node-version-check": "^1.0.1", "@11ty/nunjucks": "^4.0.0-alpha.1", "@11ty/parse-date-strings": "^2.0.6", "@11ty/posthtml-urls": "^1.0.2", "@11ty/recursive-copy": "^5.0.2", "@sindresorhus/slugify": "^3.0.0", "bcp-47-normalize": "^2.3.0", "chokidar": "^5.0.0", "debug": "^4.4.3", "dependency-graph": "^1.0.0", "entities": "^7.0.1", "import-module-string": "^2.0.3", "iso-639-1": "^3.1.5", "kleur": "^4.1.5", "liquidjs": "^10.25.0", "markdown-it": "^14.1.1", "minimist": "^1.2.8", "moo": "0.5.2", "node-retrieve-globals": "^6.0.1", "picomatch": "^4.0.3", "posthtml": "^0.16.7", "posthtml-match-helper": "^2.0.3", "semver": "^7.7.4", "tinyglobby": "^0.2.15" }, "overrides": { "fdir": "6.4.6" } } ================================================ FILE: packages/client/README.md ================================================

eleventy Logo

# `@11ty/client` The client (browser-friendly) version of `@11ty/eleventy` Eleventy, a simpler static site generator. ## ➡ [Documentation](https://www.11ty.dev/docs/) - Star [this repo on GitHub](https://github.com/11ty/eleventy/)! - Follow us [on Mastodon `@11ty@neighborhood.11ty.dev`](https://neighborhood.11ty.dev/@11ty) - Follow us [on Bluesky `@11ty.dev`](https://bsky.app/profile/11ty.dev) - Install [from npm](https://www.npmjs.com/org/11ty) - Follow [on GitHub](https://github.com/11ty) - Watch us [on YouTube](https://www.youtube.com/c/EleventyVideo) - Chat on [Discord](https://www.11ty.dev/blog/discord/) - Latest: [![npm Version](https://img.shields.io/npm/v/@11ty/client.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/client) ## Installation ``` npm install @11ty/client --save ``` With exports for: - `@11ty/client` - `@11ty/client/md` (Markdown Template Engine) - `@11ty/client/njk` (Nunjucks Template Engine) - `@11ty/client/liquid` (Liquid Template Engine) - `@11ty/client/i18n` (i18n Plugin) ================================================ FILE: packages/client/generate-bundle.js ================================================ import fs from "node:fs"; import { default as bundleClient } from "@11ty/package-bundler"; import pkg from "../../package.json" with { type: "json" }; import { readableFileSize } from "../../src/Util/FileSize.js"; const PREFIX = `[11ty/bundle/client] `; function size(filepath) { return readableFileSize(fs.statSync(filepath).size); } await bundleClient("./src/BundleCore.js", "./dist/eleventy.core.js", { name: `Eleventy v${pkg.version} (@11ty/client Bundle)`, moduleRoot: "../../", // No core-bundled plugins, reduced feature set adapterSuffixes: [".client.js", ".core.js", ".core.cjs"], external: ["node:fs", "node:crypto", "@sindresorhus/slugify"], esbuild: { keepNames: false, // minify: true }, }); console.log(`${PREFIX}Wrote dist/eleventy.core.js: ${size("./dist/eleventy.core.js")}`); // Careful, this one is big! await bundleClient("./src/BundleEleventy.js", `./dist/eleventy.js`, { name: `Eleventy v${pkg.version} (@11ty/client/eleventy Bundle)`, moduleRoot: "../../", adapterSuffixes: [".core.js", ".core.cjs"], // Adds named export FileSystem for using the file system in other packages fileSystemMode: "publish", }); console.log(`${PREFIX}Wrote dist/eleventy.js: ${size("./dist/eleventy.js")}`); // fs.mkdirSync("./visualize/", { recursive: true }); // fs.writeFileSync("./visualize/meta.json", JSON.stringify(result.metafile)); // npx esbuild-visualizer --metadata ./packages/client/visualize/meta.json --filename packages/client/visualize/index.html await bundleClient( import.meta.resolve("./src/BundleLiquid.js"), `./dist/formats/eleventy-liquid.js`, { name: `Eleventy v${pkg.version} (@11ty/client/liquid Engine Bundle)`, moduleRoot: "../../", adapterSuffixes: [".core.js", ".core.cjs"], }, ); console.log( `${PREFIX}Wrote dist/formats/eleventy-liquid.js: ${size("./dist/formats/eleventy-liquid.js")}`, ); await bundleClient( import.meta.resolve("./src/BundleNunjucks.js"), `./dist/formats/eleventy-nunjucks.js`, { name: `Eleventy v${pkg.version} (@11ty/client/njk Engine Bundle)`, moduleRoot: "../../", }, ); console.log( `${PREFIX}Wrote dist/formats/eleventy-nunjucks.js: ${size("./dist/formats/eleventy-nunjucks.js")}`, ); await bundleClient( import.meta.resolve("./src/BundleMarkdown.js"), `./dist/formats/eleventy-markdown.js`, { name: `Eleventy v${pkg.version} (@11ty/client/md Engine Bundle)`, moduleRoot: "../../", adapterSuffixes: [".core.js", ".core.cjs"], }, ); console.log( `${PREFIX}Wrote dist/formats/eleventy-markdown.js: ${size("./dist/formats/eleventy-markdown.js")}`, ); await bundleClient( import.meta.resolve("./src/BundleI18nPlugin.js"), `./dist/plugins/eleventy-plugin-i18n.js`, { name: `Eleventy v${pkg.version} (i18n Plugin)`, moduleRoot: "../../", adapterSuffixes: [".core.js", ".core.cjs"], }, ); console.log( `${PREFIX}Wrote dist/plugins/eleventy-plugin-i18n.js: ${size("./dist/plugins/eleventy-plugin-i18n.js")}`, ); ================================================ FILE: packages/client/package.json ================================================ { "name": "@11ty/client", "description": "Run Eleventy in your browser.", "version": "PRIVATE", "private": true, "publishConfig": { "access": "public", "provenance": true }, "type": "module", "main": "./dist/eleventy.core.js", "exports": { ".": "./dist/eleventy.core.js", "./eleventy": "./dist/eleventy.js", "./liquid": "./dist/formats/eleventy-liquid.js", "./njk": "./dist/formats/eleventy-nunjucks.js", "./md": "./dist/formats/eleventy-markdown.js", "./i18n": "./dist/plugins/eleventy-plugin-i18n.js" }, "files": [ "./dist/**/*.js" ], "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/11ty" }, "scripts": { "build": "node generate-bundle.js", "test": "npm run build && npx vitest", "prepare": "npm run build" }, "author": "Zach Leatherman (https://zachleat.com/)", "repository": { "type": "git", "url": "git://github.com/11ty/eleventy.git" }, "bugs": "https://github.com/11ty/eleventy/issues", "homepage": "https://www.11ty.dev/", "devDependencies": { "@11ty/package-bundler": "^0.5.5", "@vitest/browser": "^4.0.5", "@vitest/browser-playwright": "^4.0.5", "playwright": "^1.56.1", "vitest": "^4.0.5" } } ================================================ FILE: packages/client/src/BundleCore.js ================================================ // see BundleEleventy.js for Core WITH bundled Eleventy core plugins import "./shims/shim-core.js"; export { MinimalCore as Eleventy } from "../../../src/CoreMinimal.js"; ================================================ FILE: packages/client/src/BundleEleventy.js ================================================ import "./shims/shim-core.js"; // @11ty/eleventy-plugin-bundle is not exported here (differing from Node package) but *is* bundled (and exposed via Configuration API) export { IdAttributePlugin } from "../../../src/Plugins/IdAttributePlugin.js"; export { default as HtmlBasePlugin } from "../../../src/Plugins/HtmlBasePlugin.js"; export { TransformPlugin as InputPathToUrlTransformPlugin } from "../../../src/Plugins/InputPathToUrl.js"; export { default as RenderPlugin } from "../../../src/Plugins/RenderPlugin.js"; // i18n Plugin is separate (see BundleI18nPlugin.js and @11ty/client/i18n) // Note for future visitors, an attempt was made to separate these plugins the bundle (so that they could be exported separately) // - HtmlBasePlugin and InputPathToUrl were moved to async in the ResolvePlugin.js adapter. // - Extended configuration was removed from defaultConfig.js // This saved ~400KB (unmin) from the bundle but the separate bundle was way larger than the savings (> 1MB) export { Core as Eleventy } from "../../../src/Core.js"; export { default as FileSystem } from "node:fs"; ================================================ FILE: packages/client/src/BundleI18nPlugin.js ================================================ export { default as I18nPlugin } from "../../../src/Plugins/I18nPlugin.js"; ================================================ FILE: packages/client/src/BundleLiquid.js ================================================ export { default as Liquid } from "../../../src/Engines/Liquid.js"; ================================================ FILE: packages/client/src/BundleMarkdown.js ================================================ export { default as Markdown } from "../../../src/Engines/Markdown.js"; ================================================ FILE: packages/client/src/BundleNunjucks.js ================================================ export { default as Nunjucks } from "../../../src/Engines/Nunjucks.js"; ================================================ FILE: packages/client/src/shims/process.cjs ================================================ // MIT Licensed // https://github.com/defunctzombie/node-process/blob/master/browser.js // shim for using process in browser var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. var cachedSetTimeout; var cachedClearTimeout; function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } (function () { try { if (typeof setTimeout === 'function') { cachedSetTimeout = setTimeout; } else { cachedSetTimeout = defaultSetTimout; } } catch (e) { cachedSetTimeout = defaultSetTimout; } try { if (typeof clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } else { cachedClearTimeout = defaultClearTimeout; } } catch (e) { cachedClearTimeout = defaultClearTimeout; } } ()) function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.prependListener = noop; process.prependOnceListener = noop; process.listeners = function (name) { return [] } process.binding = function (name) { throw new Error('process.binding is not supported'); }; process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; ================================================ FILE: packages/client/src/shims/shim-core.js ================================================ import * as process from "./process.cjs"; // `path` polyfill needs this window.process = globalThis.process = process; // `recursive-copy` needs this (not necessary for Core.js) window.global = globalThis || window; // @11ty/eleventy needs this class Buffer { static [Symbol.hasInstance](instance) { return this.isBuffer(instance); } static isBuffer() { return false; } } window.Buffer = globalThis.Buffer = Buffer; ================================================ FILE: packages/client/test/client-core.test.js ================================================ import { Eleventy } from "../dist/eleventy.core.js"; import sharedTests from "./shared-tests.js"; sharedTests(Eleventy); ================================================ FILE: packages/client/test/client-eleventy.test.js ================================================ import { Eleventy } from "../dist/eleventy.js"; import sharedTests from "./shared-tests.js"; sharedTests(Eleventy); ================================================ FILE: packages/client/test/shared-tests.js ================================================ import { assert, test } from "vitest"; import { Markdown } from "../dist/formats/eleventy-markdown.js"; import { Liquid } from "../dist/formats/eleventy-liquid.js"; import { Nunjucks } from "../dist/formats/eleventy-nunjucks.js"; import { I18nPlugin } from "../dist/plugins/eleventy-plugin-i18n.js"; export default function(Eleventy) { test("Get version number", async () => { assert.typeOf(Eleventy.getVersion(), "string"); }); test("Markdown (no preprocessor) template", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("md", Markdown); eleventyConfig.setMarkdownTemplateEngine(false); eleventyConfig.setHtmlTemplateEngine(false); eleventyConfig.setTemplateFormats("md"); eleventyConfig.addTemplate("index.md", `# Heading`); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `

Heading

`); }); test("Markdown (via Liquid) template", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("md", Markdown); eleventyConfig.addEngine("liquid", Liquid); eleventyConfig.setTemplateFormats("md"); eleventyConfig.addTemplate("index.md", `# {{ title }}`, { title: "Heading" }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `

Heading

`); }); test("Liquid template", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("liquid", Liquid); eleventyConfig.setTemplateFormats("liquid"); eleventyConfig.addTemplate("index.liquid", `

{{ title }}

`, { title: "Heading" }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `

Heading

`); }); test("Nunjucks template", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("njk", Nunjucks); eleventyConfig.setTemplateFormats("njk"); eleventyConfig.addTemplate("index.njk", `

{{ title }}

`, { title: "Heading" }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `

Heading

`); }); test("i18n Plugin Use (with 11ty.js)", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addPlugin(I18nPlugin, { defaultLanguage: "en" }); eleventyConfig.addTemplate("./en/index.11ty.js", function (data) { return `Home`; }); eleventyConfig.addTemplate("./es/index.11ty.js", function (data) { return `Home`; }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `Home`); assert.strictEqual(json[1].content.trim(), `Home`); }); // Careful, `@11ty/client` will resolve slugify via Vite instead of it bundled with the package test("slugify Filter in Liquid", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("liquid", Liquid); eleventyConfig.setTemplateFormats("liquid"); eleventyConfig.addTemplate("index.liquid", `{{ title | slugify }}`, { title: "This is a heading" }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `this-is-a-heading`); }); // Careful, `@11ty/client` will resolve slugify via Vite instead of it bundled with the package test("slugify Filter in Nunjucks", async () => { let elev = new Eleventy({ config(eleventyConfig) { eleventyConfig.addEngine("njk", Nunjucks); eleventyConfig.setTemplateFormats("njk"); eleventyConfig.addTemplate("index.njk", `{{ title | slugify }}`, { title: "This is a heading" }); } }); let json = await elev.toJSON(); assert.strictEqual(json[0].content.trim(), `this-is-a-heading`); }); } ================================================ FILE: packages/client/update-package-json.js ================================================ // Intended to run from repository root in release.sh script import fs from "node:fs"; import corePkg from "../../package.json" with { type: "json" }; // assign new version in local package.json from core package.json import clientPkg from "./package.json" with { type: "json" }; clientPkg.version = corePkg.version; delete clientPkg.private; // allow publish if ( corePkg.name !== "@11ty/eleventy" || clientPkg.name !== "@11ty/client" || !fs.existsSync("./packages/client/package.json") ) { throw new Error("Did you run this script from the wrong directory?"); } fs.writeFileSync("./packages/client/package.json", JSON.stringify(clientPkg, null, 2), "utf8"); console.log(`[11ty/bundle/client] Updated @11ty/client package version to ${corePkg.version}`); ================================================ FILE: packages/client/vitest.config.js ================================================ import { defineConfig } from "vitest/config"; import { playwright } from "@vitest/browser-playwright"; import os from "node:os"; let instances = [{ browser: "chromium" }, { browser: "firefox" }]; if (os.type() === "Darwin") { instances.push({ browser: "webkit" }); } export default defineConfig({ test: { browser: { enabled: true, provider: playwright(), headless: true, screenshotFailures: false, // https://vitest.dev/guide/browser/playwright instances, }, }, }); ================================================ FILE: scripts/release-dryrun.sh ================================================ export NPM_PUBLISH_TAG="canary" export DRY_RUN="--dry-run" # leave that space as-is echo "Running @11ty/eleventy and @11ty/client publish dry run test" ./scripts/release.sh ================================================ FILE: scripts/release.sh ================================================ if ! npm ci; then echo 'Release error: npm ci command failed.' exit 1 fi if ! npx playwright install; then echo 'Release error: npx playwright install command failed (for Vitest Browser Mode).' exit 1 fi # This step includes running packages/ test suites if ! npm test; then echo 'Release error: npm test command failed.' exit 1 fi if [ -z "$NPM_PUBLISH_TAG" ]; then echo 'Release error: missing NPM_PUBLISH_TAG environment variable' exit 1 fi node packages/client/update-package-json.js # Will skip publishing root if publishing workspaces fails if npm publish --workspaces --provenance --access=public --tag=$NPM_PUBLISH_TAG $DRY_RUN; then npm publish --provenance --access=public --tag=$NPM_PUBLISH_TAG $DRY_RUN fi ================================================ FILE: src/Adapters/Engines/Liquid.core.js ================================================ // Must load dynamically in standard core export default undefined; ================================================ FILE: src/Adapters/Engines/Liquid.js ================================================ export { default } from "../../Engines/Liquid.js"; ================================================ FILE: src/Adapters/Engines/Markdown.core.js ================================================ // Must load dynamically in standard core export default undefined; ================================================ FILE: src/Adapters/Engines/Markdown.js ================================================ export { default } from "../../Engines/Markdown.js"; ================================================ FILE: src/Adapters/Engines/Nunjucks.core.js ================================================ // Must load dynamically in standard core export default undefined; ================================================ FILE: src/Adapters/Engines/Nunjucks.js ================================================ export { default } from "../../Engines/Nunjucks.js"; ================================================ FILE: src/Adapters/Packages/chalk.client.js ================================================ function noop(arg) { return arg; } export default { bold: noop, red: noop, gray: noop, cyan: noop, yellow: noop, }; ================================================ FILE: src/Adapters/Packages/chalk.js ================================================ import chalk from "kleur"; export default chalk; ================================================ FILE: src/Adapters/Packages/inspect.core.js ================================================ export function inspect(target) { return JSON.stringify(target, null, 2); } ================================================ FILE: src/Adapters/Packages/inspect.js ================================================ import { inspect as nodeInspect } from "node:util"; export function inspect(target) { return nodeInspect(target, { showHidden: false, depth: null }); } ================================================ FILE: src/Adapters/Packages/semver.client.js ================================================ export function satisfies(version, range) { // Always return true return true; } ================================================ FILE: src/Adapters/Packages/semver.js ================================================ // Costs ~18 KB import semverSatisfies from "semver/functions/satisfies.js"; export { semverSatisfies as satisfies }; ================================================ FILE: src/Adapters/Packages/url.core.js ================================================ import path from "node:path"; // TODO move this into package-bundler as a shim? // Thank you bare-url! // Apache-2.0 LICENSE https://github.com/holepunchto/bare-url/blob/main/LICENSE export function fileURLToPath(url) { if (typeof url === "string") { url = new URL(url); } if (url.protocol !== "file:") { throw new Error("The URL must use the file: protocol"); } if (url.hostname) { throw new Error("The file: URL host must be 'localhost' or empty"); } if (/%2f/i.test(url.pathname)) { throw new Error("The file: URL path must not include encoded / characters"); } return path.normalize(decodeURIComponent(url.pathname)); } ================================================ FILE: src/Adapters/Packages/url.js ================================================ export { fileURLToPath } from "node:url"; ================================================ FILE: src/Adapters/getDefaultConfig.core.js ================================================ import defaultConfig from "../defaultConfig.js"; // Standard and minimal bundle import directly for bundling export default async function () { return defaultConfig; } ================================================ FILE: src/Adapters/getDefaultConfig.js ================================================ // Standard export default async function () { return import("../defaultConfig.js").then((mod) => mod.default); } ================================================ FILE: src/Benchmark/Benchmark.js ================================================ export default class Benchmark { constructor() { // TypeScript slop this.timeSpent = 0; this.timesCalled = 0; this.beforeTimers = []; } reset() { this.timeSpent = 0; this.timesCalled = 0; this.beforeTimers = []; } getNewTimestamp() { if (performance) { return performance.now(); } return new Date().getTime(); } incrementCount() { this.timesCalled++; } // TODO(slightlyoff): // disable all of these hrtime requests when not benchmarking before() { this.timesCalled++; this.beforeTimers.push(this.getNewTimestamp()); } after() { if (!this.beforeTimers.length) { throw new Error("You called Benchmark after() without a before()."); } let before = this.beforeTimers.pop(); if (!this.beforeTimers.length) { this.timeSpent += this.getNewTimestamp() - before; } } getTimesCalled() { return this.timesCalled; } getTotal() { return this.timeSpent; } } ================================================ FILE: src/Benchmark/BenchmarkGroup.js ================================================ import debugUtil from "debug"; import ConsoleLogger from "../Util/ConsoleLogger.js"; import isAsyncFunction from "../Util/IsAsyncFunction.js"; import Benchmark from "./Benchmark.js"; const debugBenchmark = debugUtil("Eleventy:Benchmark"); class BenchmarkGroup { constructor() { this.benchmarks = {}; // Warning: aggregate benchmarks automatically default to false via BenchmarkManager->getBenchmarkGroup this.isVerbose = true; this.logger = new ConsoleLogger(); this.minimumThresholdMs = 50; this.minimumThresholdPercent = 8; } setIsVerbose(isVerbose) { this.isVerbose = isVerbose; this.logger.isVerbose = isVerbose; } reset() { for (var type in this.benchmarks) { this.benchmarks[type].reset(); } } // TODO use addAsync everywhere instead add(type, callback) { let benchmark = (this.benchmarks[type] = new Benchmark()); /** @this {any} */ let fn = function (...args) { benchmark.before(); let ret = callback.call(this, ...args); benchmark.after(); return ret; }; Object.defineProperty(fn, "__eleventyInternal", { value: { type: isAsyncFunction(callback) ? "async" : "sync", callback, }, }); return fn; } // callback must return a promise // async addAsync(type, callback) { // let benchmark = (this.benchmarks[type] = new Benchmark()); // benchmark.before(); // // don’t await here. // let promise = callback.call(this); // promise.then(function() { // benchmark.after(); // }); // return promise; // } setMinimumThresholdMs(minimumThresholdMs) { let val = parseInt(minimumThresholdMs, 10); if (isNaN(val)) { throw new Error("`setMinimumThresholdMs` expects a number argument."); } this.minimumThresholdMs = val; } setMinimumThresholdPercent(minimumThresholdPercent) { let val = parseInt(minimumThresholdPercent, 10); if (isNaN(val)) { throw new Error("`setMinimumThresholdPercent` expects a number argument."); } this.minimumThresholdPercent = val; } has(type) { return !!this.benchmarks[type]; } get(type) { if (!this.benchmarks[type]) { this.benchmarks[type] = new Benchmark(); } return this.benchmarks[type]; } padNumber(num, length) { if (("" + num).length >= length) { return num; } let prefix = new Array(length + 1).join(" "); return (prefix + num).slice(-1 * length); } finish(label, totalTimeSpent) { for (var type in this.benchmarks) { let bench = this.benchmarks[type]; let isAbsoluteMinimumComparison = this.minimumThresholdMs > 0; let totalForBenchmark = bench.getTotal(); let percent = Math.round((totalForBenchmark * 100) / totalTimeSpent); let callCount = bench.getTimesCalled(); let output = { ms: this.padNumber(totalForBenchmark.toFixed(0), 6), percent: this.padNumber(percent, 3), calls: this.padNumber(callCount, 5), }; let str = `Benchmark ${output.ms}ms ${output.percent}% ${output.calls}× (${label}) ${type}`; if ( isAbsoluteMinimumComparison && totalForBenchmark >= this.minimumThresholdMs && percent > this.minimumThresholdPercent ) { this.logger.warn(str); } // Opt out of logging if low count (1× or 2×) or 0ms / 1% if ( callCount > 1 || // called more than once Math.round(totalForBenchmark) > 0 // more than 0.5ms ) { debugBenchmark(str); } } } } export default BenchmarkGroup; ================================================ FILE: src/Benchmark/BenchmarkManager.js ================================================ import BenchmarkGroup from "./BenchmarkGroup.js"; // TODO this should not be a singleton, it belongs in the config or somewhere on the Eleventy instance. class BenchmarkManager { constructor() { this.benchmarkGroups = {}; this.isVerbose = true; this.start = this.getNewTimestamp(); } reset() { this.start = this.getNewTimestamp(); for (var j in this.benchmarkGroups) { this.benchmarkGroups[j].reset(); } } getNewTimestamp() { if (performance) { return performance.now(); } return new Date().getTime(); } setVerboseOutput(isVerbose) { this.isVerbose = !!isVerbose; } hasBenchmarkGroup(name) { return name in this.benchmarkGroups; } getBenchmarkGroup(name) { if (!this.benchmarkGroups[name]) { this.benchmarkGroups[name] = new BenchmarkGroup(); // Special behavior for aggregate benchmarks // so they don’t console.log every time if (name === "Aggregate") { this.benchmarkGroups[name].setIsVerbose(false); } else { this.benchmarkGroups[name].setIsVerbose(this.isVerbose); } } return this.benchmarkGroups[name]; } getAll() { return this.benchmarkGroups; } get(name) { if (name) { return this.getBenchmarkGroup(name); } return this.getAll(); } finish() { let totalTimeSpentBenchmarking = this.getNewTimestamp() - this.start; for (var j in this.benchmarkGroups) { this.benchmarkGroups[j].finish(j, totalTimeSpentBenchmarking); } } } export default BenchmarkManager; ================================================ FILE: src/Core.js ================================================ import { MinimalCore } from "./CoreMinimal.js"; import FileSystemSearch from "./FileSystemSearch.js"; import EleventyFiles from "./EleventyFiles.js"; import TemplatePassthroughManager from "./TemplatePassthroughManager.js"; // Core with File System support (but without Dev Server or Chokidar or Bundled Plugins) export class Core extends MinimalCore { async initializeConfig(initOverrides) { await super.initializeConfig(initOverrides); /** @type {object} */ this.fileSystemSearch = new FileSystemSearch(); } async init(options = {}) { await super.init(options); this.templateData.setFileSystemSearch(this.fileSystemSearch); this.passthroughManager = new TemplatePassthroughManager(this.eleventyConfig); this.passthroughManager.setRunMode(this.runMode); this.passthroughManager.setDryRun(this.isDryRun); this.passthroughManager.extensionMap = this.extensionMap; this.passthroughManager.setFileSystemSearch(this.fileSystemSearch); let formats = this.templateFormats.getTemplateFormats(); this.eleventyFiles = new EleventyFiles(formats, this.eleventyConfig); this.eleventyFiles.setPassthroughManager(this.passthroughManager); this.eleventyFiles.setFileSystemSearch(this.fileSystemSearch); this.eleventyFiles.setRunMode(this.runMode); this.eleventyFiles.extensionMap = this.extensionMap; // This needs to be set before init or it’ll construct a new one this.eleventyFiles.templateData = this.templateData; this.eleventyFiles.init(); this.writer.setPassthroughManager(this.passthroughManager); this.writer.setEleventyFiles(this.eleventyFiles); } /** * Restarts Eleventy. */ async restart() { await super.restart(); // TODO this.passthroughManager.reset(); // TODO this.eleventyFiles.restart(); } } ================================================ FILE: src/CoreMinimal.js ================================================ import debugUtil from "debug"; import { isPlainObject, TemplatePath } from "@11ty/eleventy-utils"; import chalk from "./Adapters/Packages/chalk.js"; import TemplateData from "./Data/TemplateData.js"; import TemplateWriter from "./TemplateWriter.js"; import EleventyExtensionMap from "./EleventyExtensionMap.js"; import { EleventyErrorHandler } from "./Errors/EleventyErrorHandler.js"; import TemplateConfig from "./TemplateConfig.js"; import TemplateEngineManager from "./Engines/TemplateEngineManager.js"; /* Utils */ import { readableFileSize } from "./Util/FileSize.js"; import simplePlural from "./Util/Pluralize.js"; import ConsoleLogger from "./Util/ConsoleLogger.js"; import ProjectDirectories from "./Util/ProjectDirectories.js"; import { getEleventyPackageJson, importJsonSync, getWorkingProjectPackageJsonPath, } from "./Util/ImportJsonSync.js"; import ProjectTemplateFormats from "./Util/ProjectTemplateFormats.js"; const pkg = getEleventyPackageJson(); const debug = debugUtil("Eleventy"); /** * Eleventy’s programmatic API * @module 11ty/eleventy/Eleventy */ export class MinimalCore { /** * Userspace package.json file contents * @type {object|undefined} */ #projectPackageJson; /** @type {string} */ #projectPackageJsonPath; /** @type {ProjectTemplateFormats|undefined} */ #templateFormats; /** @type {ConsoleLogger|undefined} */ #logger; /** @type {ProjectDirectories|undefined} */ #directories; /** @type {boolean|undefined} */ #verboseOverride; /** @type {boolean} */ #isVerboseMode = true; /** @type {boolean|undefined} */ #preInitVerbose; /** @type {boolean} */ #hasConfigInitialized = false; /** @type {boolean} */ #needsInit = true; /** @type {Promise|undefined} */ #initPromise; /** @type {EleventyErrorHandler|undefined} */ #errorHandler; /** @type {Map} */ #privateCaches = new Map(); /** @type {boolean|undefined} */ #isEsm; /** @type {string} */ #activeConfigurationPath; // Support both new Eleventy(options) and new Eleventy(input, output, options) #normalizeConstructorArguments(...args) { let input; let output; let options; let eleventyConfig; if (isPlainObject(args[0])) { options = args[0] || {}; input = options.input; output = options.output; eleventyConfig = args[1]; } else { input = args[0]; output = args[1]; options = args[2] || {}; eleventyConfig = args[3]; } return { input, output, options, eleventyConfig, }; } /** * @typedef {object} EleventyOptions * @property {'cli'|'script'=} source * @property {'build'|'serve'|'watch'=} runMode * @property {boolean=} dryRun * @property {string=} configPath * @property {string=} pathPrefix * @property {boolean=} quietMode * @property {Function=} config * @property {string=} inputDir * @param {string} [input] - Directory or filename for input/sources files. * @param {string} [output] - Directory serving as the target for writing the output files. * @param {EleventyOptions} [options={}] * @param {TemplateConfig} [eleventyConfig] */ constructor(...args) { let { input, output, options = {}, eleventyConfig = null, } = this.#normalizeConstructorArguments(...args); /** * @type {string|undefined} * @description Holds the path to the input (might be a file or folder) */ this.rawInput = input || undefined; /** * @type {string|undefined} * @description holds the path to the output directory */ this.rawOutput = output || undefined; /** * @type {module:11ty/eleventy/TemplateConfig} * @description Override the config instance (for centralized config re-use) */ this.eleventyConfig = eleventyConfig; /** * @type {EleventyOptions} * @description Options object passed to the Eleventy constructor * @default {} */ this.options = options; /** * @type {'cli'|'script'} * @description Called via CLI (`cli`) or Programmatically (`script`) * @default "script" */ this.source = options.source || "script"; /** * @type {string} * @description One of build, serve, or watch * @default "build" */ this.runMode = options.runMode || "build"; /** * @type {boolean} * @description Is Eleventy running in dry mode? * @default false */ this.isDryRun = options.dryRun ?? false; /** * @type {boolean} * @description Is this an incremental build? (only operates on a subset of input files) * @default false */ this.isIncremental = false; /** * @type {string|undefined} * @description If an incremental build, this is the file we’re operating on. * @default null */ this.programmaticApiIncrementalFile = undefined; /** * @type {boolean} * @description Should we process files on first run? (The --ignore-initial feature) * @default true */ this.isRunInitialBuild = true; /** * @type {Number} * @description Number of builds run on this instance. * @default 0 */ this.buildCount = 0; /** * @member {String} - Force ESM or CJS mode instead of detecting from package.json. Either cjs, esm, or auto. * @default "auto" */ this.loader = this.options.loader ?? "auto"; /** * @type {Number} * @description The timestamp of Eleventy start. */ this.start = this.getNewTimestamp(); } /** * @type {string|undefined} * @description An override of Eleventy's default config file paths * @default undefined */ get configPath() { return this.options.configPath; } /** * @type {string} * @description The top level directory the site pretends to reside in * @default "/" */ get pathPrefix() { return this.options.pathPrefix || "/"; } /** * Reads the version of Eleventy. * * @static * @returns {string} - The version of Eleventy. */ static getVersion() { return pkg.version; } /** * @deprecated since 1.0.1, use static Eleventy.getVersion() */ getVersion() { return MinimalCore.getVersion(); } async initializeConfig(initOverrides) { if (!this.eleventyConfig) { this.eleventyConfig = new TemplateConfig(null, this.configPath); } else if (this.configPath) { await this.eleventyConfig.setProjectConfigPath(this.configPath); } this.#activeConfigurationPath = this.configPath ?? this.eleventyConfig.getLocalProjectConfigFile(); this.eleventyConfig.setRunMode(this.runMode); this.eleventyConfig.setProjectUsingEsm(this.isEsm); this.eleventyConfig.setLogger(this.logger); this.eleventyConfig.setDirectories(this.directories); this.eleventyConfig.setTemplateFormats(this.templateFormats); if (this.pathPrefix || this.pathPrefix === "") { this.eleventyConfig.setPathPrefix(this.pathPrefix); } // Debug mode should always run quiet (all output goes to debug logger) if (process.env.DEBUG) { this.#verboseOverride = false; } else if (this.options.quietMode === true || this.options.quietMode === false) { this.#verboseOverride = !this.options.quietMode; } // Moved before config merges: https://github.com/11ty/eleventy/issues/3316 if (this.#verboseOverride === true || this.#verboseOverride === false) { this.eleventyConfig.userConfig._setQuietModeOverride(!this.#verboseOverride); } this.eleventyConfig.userConfig.directories = this.directories; /* Programmatic API config */ if (this.options.config && typeof this.options.config === "function") { debug("Running options.config configuration callback (passed to Eleventy constructor)"); // TODO use return object here? await this.options.config(this.eleventyConfig.userConfig); } /** * @type {object} * @description Initialize Eleventy environment variables * @default null */ // this.runMode need to be set before this this.env = this.getEnvironmentVariableValues(); this.initializeEnvironmentVariables(this.env); // Async initialization of configuration await this.eleventyConfig.init(initOverrides); /** * @type {object} * @description Initialize Eleventy’s configuration, including the user config file */ this.config = this.eleventyConfig.getConfig(); /** * @type {object} * @description Singleton BenchmarkManager instance */ this.bench = this.config.benchmarkManager; if (performance) { debug("Eleventy warm up time: %o (ms)", performance.now()); } this.#hasConfigInitialized = true; // after #hasConfigInitialized above this.setIsVerbose(this.#preInitVerbose ?? !this.config.quietMode); } getNewTimestamp() { if (performance) { return performance.now(); } return new Date().getTime(); } /** @type {ProjectDirectories} */ get directories() { if (!this.#directories) { this.#directories = new ProjectDirectories(); this.#directories.setInput(this.rawInput, this.options.inputDir); this.#directories.setOutput(this.rawOutput); if (this.source == "cli" && (this.rawInput !== undefined || this.rawOutput !== undefined)) { this.#directories.freeze(); } } return this.#directories; } /** @type {string} */ get input() { return this.directories.inputFile || this.directories.input || this.config.dir.input; } /** @type {string} */ get inputFile() { return this.directories.inputFile; } /** @type {string} */ get inputDir() { return this.directories.input; } // Not used internally, removed in 3.0. setInputDir() { throw new Error( "Eleventy->setInputDir was removed in 3.0. Use the inputDir option to the constructor", ); } /** @type {string} */ get outputDir() { return this.directories.output || this.config.dir.output; } /** * Updates the dry-run mode of Eleventy. * * @param {boolean} isDryRun - Shall Eleventy run in dry mode? */ setDryRun(isDryRun) { this.isDryRun = !!isDryRun; } /** * Sets the incremental build mode. * * @param {boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates */ setIncrementalBuild(isIncremental) { this.isIncremental = !!isIncremental; if (this.writer) { this.writer.setIncrementalBuild(this.isIncremental); } } /** * Set whether or not to do an initial build * * @param {boolean} ignoreInitialBuild - Shall Eleventy ignore the default initial build before watching in watch/serve mode? * @default true */ setIgnoreInitial(ignoreInitialBuild) { this.isRunInitialBuild = !ignoreInitialBuild; if (this.writer) { this.writer.setRunInitialBuild(this.isRunInitialBuild); } } /** * Updates the path prefix used in the config. * * @param {string} pathPrefix - The new path prefix. */ setPathPrefix(pathPrefix) { if (pathPrefix || pathPrefix === "") { this.eleventyConfig.setPathPrefix(pathPrefix); // TODO reset config // this.config = this.eleventyConfig.getConfig(); } } /** * Restarts Eleventy. */ async restart() { debug("Restarting."); this.start = this.getNewTimestamp(); this.extensionMap.reset(); this.bench.reset(); } #cache(key, inst) { if (!("caches" in inst)) { throw new Error("To use #cache you need a `caches` getter object"); } // Restore from cache if (this.#privateCaches.has(key)) { let c = this.#privateCaches.get(key); for (let cacheKey in c) { inst[cacheKey] = c[cacheKey]; } } else { // Set cache let c = {}; for (let cacheKey of inst.caches || []) { c[cacheKey] = inst[cacheKey]; } this.#privateCaches.set(key, c); } } /** * Starts Eleventy. */ async init(options = {}) { let { viaConfigReset } = Object.assign({ viaConfigReset: false }, options); if (!this.#hasConfigInitialized) { await this.initializeConfig(); } else { // Note: Global event bus is different from user config event bus this.config.events.reset(); } await this.config.events.emit("eleventy.config", this.eleventyConfig); if (this.env) { await this.config.events.emit("eleventy.env", this.env); } let formats = this.templateFormats.getTemplateFormats(); let engineManager = new TemplateEngineManager(this.eleventyConfig); this.extensionMap = new EleventyExtensionMap(this.eleventyConfig); this.extensionMap.setFormats(formats); this.extensionMap.engineManager = engineManager; await this.config.events.emit("eleventy.extensionmap", this.extensionMap); this.templateData = new TemplateData(this.eleventyConfig); this.templateData.setProjectUsingEsm(this.isEsm); this.templateData.extensionMap = this.extensionMap; if (this.env) { this.templateData.environmentVariables = this.env; } // Note these directories are all project root relative this.config.events.emit("eleventy.directories", this.directories.getUserspaceInstance()); this.writer = new TemplateWriter(formats, this.templateData, this.eleventyConfig); if (!viaConfigReset) { // set or restore cache this.#cache("TemplateWriter", this.writer); } this.writer.logger = this.logger; this.writer.extensionMap = this.extensionMap; this.writer.setRunInitialBuild(this.isRunInitialBuild); this.writer.setIncrementalBuild(this.isIncremental); let debugStr = `Directories: Input: Directory: ${this.directories.input} File: ${this.directories.inputFile || false} Glob: ${this.directories.inputGlob || false} Data: ${this.directories.data} Includes: ${this.directories.includes} Layouts: ${this.directories.layouts || false} Output: ${this.directories.output} Template Formats: ${formats.join(",")} Verbose Output: ${this.verboseMode}`; debug(debugStr); this.writer.setVerboseOutput(this.verboseMode); this.writer.setDryRun(this.isDryRun); this.#needsInit = false; } // These are all set as initial global data under eleventy.env.* (see TemplateData->environmentVariables) getEnvironmentVariableValues() { let values = { source: this.source, runMode: this.runMode, }; if (this.#activeConfigurationPath) { values.config = TemplatePath.absolutePath(this.#activeConfigurationPath); } // Fixed: instead of configuration directory, explicit root or working directory values.root = TemplatePath.getWorkingDir(); values.source = this.source; // Backwards compatibility Object.defineProperty(values, "isServerless", { enumerable: false, value: false, }); return values; } /** * Set process.ENV variables for use in Eleventy projects * * @method */ initializeEnvironmentVariables(env) { // Recognize that global data `eleventy.version` is coerced to remove prerelease tags // and this is the raw version (3.0.0 versus 3.0.0-alpha.6). // `eleventy.env.version` does not yet exist (unnecessary) process.env.ELEVENTY_VERSION = MinimalCore.getVersion(); process.env.ELEVENTY_ROOT = env.root; debug("Setting process.env.ELEVENTY_ROOT: %o", env.root); process.env.ELEVENTY_SOURCE = env.source; process.env.ELEVENTY_RUN_MODE = env.runMode; } /** @param {boolean} value */ set verboseMode(value) { this.setIsVerbose(value); } /** @type {boolean} */ get verboseMode() { return this.#isVerboseMode; } /** @type {ConsoleLogger} */ get logger() { if (!this.#logger) { this.#logger = new ConsoleLogger(); this.#logger.isVerbose = this.verboseMode; } return this.#logger; } /** @param {ConsoleLogger} logger */ set logger(logger) { this.eleventyConfig.setLogger(logger); this.#logger = logger; } disableLogger() { this.logger.overrideLogger(false); } /** @type {EleventyErrorHandler} */ get errorHandler() { if (!this.#errorHandler) { this.#errorHandler = new EleventyErrorHandler(); this.#errorHandler.isVerbose = this.verboseMode; this.#errorHandler.logger = this.logger; } return this.#errorHandler; } /** * Updates the verbose mode of Eleventy. * * @method * @param {boolean} isVerbose - Shall Eleventy run in verbose mode? */ setIsVerbose(isVerbose) { if (!this.#hasConfigInitialized) { this.#preInitVerbose = !!isVerbose; return; } // always defer to --quiet if override happened isVerbose = this.#verboseOverride ?? !!isVerbose; this.#isVerboseMode = isVerbose; if (this.logger) { this.logger.isVerbose = isVerbose; } this.bench.setVerboseOutput(isVerbose); if (this.writer) { this.writer.setVerboseOutput(isVerbose); } if (this.errorHandler) { this.errorHandler.isVerbose = isVerbose; } // Set verbose mode in config file this.eleventyConfig.verbose = isVerbose; } get templateFormats() { if (!this.#templateFormats) { let tf = new ProjectTemplateFormats(); this.#templateFormats = tf; } return this.#templateFormats; } /** * Updates the template formats of Eleventy. * * @method * @param {string} formats - The new template formats. */ setFormats(formats) { this.templateFormats.setViaCommandLine(formats); } /** * Updates the run mode of Eleventy. * * @method * @param {string} runMode - One of "build", "watch", or "serve" */ setRunMode(runMode) { this.runMode = runMode; } /** * Set the file that needs to be rendered/compiled/written for an incremental build. * This method is also wired up to the CLI --incremental=incrementalFile * * @method * @param {string} incrementalFile - File path (added or modified in a project) */ setIncrementalFile(incrementalFile) { if (incrementalFile) { // This used to also setIgnoreInitial(true) but was changed in 3.0.0-alpha.14 this.setIncrementalBuild(true); this.programmaticApiIncrementalFile = TemplatePath.addLeadingDotSlash(incrementalFile); // Used to determind template relevance for compile cache keys this.eleventyConfig.setPreviousBuildModifiedFile(incrementalFile); } } unsetIncrementalFile() { // only applies to initial build, no re-runs (--watch/--serve) if (this.programmaticApiIncrementalFile) { // this.setIgnoreInitial(false); this.programmaticApiIncrementalFile = undefined; } // reset back to false this.setIgnoreInitial(false); } /** * Resets the config of Eleventy. * * @method */ async resetConfig() { delete this.eleventyConfig; // ensures `initializeConfig()` will run when `init()` is called next this.#hasConfigInitialized = false; } // fetch from project’s package.json get projectPackageJsonPath() { if (this.#projectPackageJsonPath === undefined) { this.#projectPackageJsonPath = getWorkingProjectPackageJsonPath() || false; } return this.#projectPackageJsonPath; } get projectPackageJson() { if (!this.#projectPackageJson) { let p = this.projectPackageJsonPath; this.#projectPackageJson = p ? importJsonSync(p) : {}; } return this.#projectPackageJson; } get isEsm() { if (this.#isEsm !== undefined) { return this.#isEsm; } if (this.loader == "esm") { this.#isEsm = true; } else if (this.loader == "cjs") { this.#isEsm = false; } else if (this.loader == "auto") { // Note: Node defaults to CommonJS if missing, Deno defaults to ESM // https://docs.deno.com/runtime/fundamentals/node/#commonjs-support if (typeof Deno !== "undefined") { this.#isEsm = this.projectPackageJson?.type !== "commonjs"; } else { this.#isEsm = this.projectPackageJson?.type === "module"; } } else { throw new Error("The 'loader' option must be one of 'esm', 'cjs', or 'auto'"); } return this.#isEsm; } /** * Writes templates to the file system. * * @async * @method * @param {String} subtype - (optional) or "templates" (skips passthrough copy) or "copy" (skips templates) * @returns {Promise<{Array}>} */ async write(subtype) { if (subtype) { if (subtype !== "fs" && !subtype?.startsWith("fs:")) { subtype = `fs:${subtype}`; } return this.executeBuild(subtype); } return this.executeBuild("fs"); } /** * Renders templates to a JSON object. * * @async * @method * @returns {Promise<{Array}>} */ async toJSON() { return this.executeBuild("json"); } toNDJSON() { throw new Error("Feature removed in Eleventy v4: https://github.com/11ty/eleventy/issues/3382"); } /** * tbd. * * @async * @method * @returns {Promise<{Array}>} ret - tbd. */ async executeBuild(to = "fs") { if (this.#needsInit) { if (!this.#initPromise) { this.#initPromise = this.init(); } await this.#initPromise.then(() => { // #needsInit also set to false at the end of `init()` this.#needsInit = false; this.#initPromise = undefined; }); } if (!this.writer) { throw new Error( "Internal error: Eleventy didn’t run init() properly and wasn’t able to create a TemplateWriter.", ); } let incrementalFile = this.programmaticApiIncrementalFile || this.watchQueue?.getIncrementalFile(); if (incrementalFile) { this.writer.setIncrementalFile(incrementalFile); } let returnObj; let hasError = false; let outputMode = String(to); // normalize fs:templates or fs:copy to `fs` if (outputMode.includes(":")) { outputMode = outputMode.split(":").shift(); } try { let directories = this.directories.getUserspaceInstance(); let eventsArg = { directories, // v3.0.0-alpha.6, changed to use `directories` instead (this was only used by serverless plugin) inputDir: directories.input, // Deprecated (not normalized) use `directories` instead. dir: this.config.dir, runMode: this.runMode, outputMode, incremental: this.isIncremental, }; await this.config.events.emit("beforeBuild", eventsArg); await this.config.events.emit("eleventy.before", eventsArg); let promise; if (to === "fs") { promise = this.writer.write(); } else if (to === "fs:templates") { promise = this.writer.writeTemplates(); } else if (to === "json") { promise = this.writer.getJSON("json"); } else { throw new Error( `Invalid argument for \`Eleventy->executeBuild(${to})\`, expected "json", "fs", or "fs:templates".`, ); } let resolved = await promise; // Passing the processed output to the eleventy.after event (2.0+) eventsArg.results = resolved.templates; if (to === "json" || to === "fs:templates") { // Backwards compat returnObj = resolved.templates; } else { // Backwards compat returnObj = [resolved.passthroughCopy, resolved.templates]; } this.unsetIncrementalFile(); this.writer.resetIncrementalFile(); eventsArg.uses = this.eleventyConfig.usesGraph.map; await this.config.events.emit("afterBuild", eventsArg); await this.config.events.emit("eleventy.after", eventsArg); this.buildCount++; } catch (error) { hasError = true; // Issue #2405: Don’t change the exitCode for programmatic scripts let errorSeverity = this.source === "script" ? "error" : "fatal"; this.errorHandler.once(errorSeverity, error, "Problem writing Eleventy templates"); throw error; } finally { this.bench.finish(); if (outputMode === "fs") { this.logger.logWithOptions({ message: this.logFinished(), color: hasError ? "red" : "green", force: true, }); } debug("Finished."); debug(` Have a suggestion/feature request/feedback? Feeling frustrated? I want to hear it! Open an issue: https://github.com/11ty/eleventy/issues/new`); } return returnObj; } /** * Logs some statistics after a complete run of Eleventy. * * @returns {string} ret - The log message. */ logFinished() { if (!this.writer) { throw new Error( "Did you call Eleventy.init to create the TemplateWriter instance? Hint: you probably didn’t.", ); } let ret = []; let { copyCount, copySize, skipCount, writeCount, // renderCount, // files that render (costly) but may not write to disk } = this.writer.getMetadata(); let slashRet = []; if (copyCount) { debug("Total passthrough copy aggregate size: %o", readableFileSize(copySize)); slashRet.push(`Copied ${chalk.bold(copyCount)}`); } slashRet.push( `Wrote ${chalk.bold(writeCount)} ${simplePlural(writeCount, "file", "files")}${ skipCount ? ` (skipped ${skipCount})` : "" }`, ); // slashRet.push( // `${renderCount} rendered` // ) if (slashRet.length) { ret.push(slashRet.join(" ")); } let time = (this.getNewTimestamp() - this.start) / 1000; ret.push( `in ${chalk.bold(time.toFixed(2))} ${simplePlural(time.toFixed(2), "second", "seconds")}`, ); let cfgStr = this.#activeConfigurationPath ? `, ${TemplatePath.stripLeadingDotSlash(this.#activeConfigurationPath)}` : " no config file"; // More than 1 second total, show estimate of per-template time if (time >= 1 && writeCount > 1) { ret.push( chalk.gray(`(${((time * 1000) / writeCount).toFixed(1)}ms each, v${pkg.version}${cfgStr})`), ); } else { ret.push(chalk.gray(`(v${MinimalCore.getVersion()}${cfgStr})`)); } return ret.join(" "); } } ================================================ FILE: src/Data/ComputedData.js ================================================ import lodash from "@11ty/lodash-custom"; import debugUtil from "debug"; import ComputedDataQueue from "./ComputedDataQueue.js"; import ComputedDataTemplateString from "./ComputedDataTemplateString.js"; import ComputedDataProxy from "./ComputedDataProxy.js"; const { set: lodashSet, get: lodashGet } = lodash; const debug = debugUtil("Eleventy:ComputedData"); class ComputedData { constructor(config) { this.computed = {}; this.symbolParseFunctions = {}; this.templateStringKeyLookup = {}; this.computedKeys = new Set(); this.declaredDependencies = {}; this.queue = new ComputedDataQueue(); this.config = config; } add(key, renderFn, declaredDependencies = [], symbolParseFn, templateInstance) { this.computedKeys.add(key); this.declaredDependencies[key] = declaredDependencies; // bind config filters/JS functions if (typeof renderFn === "function") { let fns = {}; // TODO bug? no access to non-universal config things? if (this.config) { fns = { ...this.config.javascriptFunctions, }; } fns.tmpl = templateInstance; renderFn = renderFn.bind(fns); } lodashSet(this.computed, key, renderFn); if (symbolParseFn) { lodashSet(this.symbolParseFunctions, key, symbolParseFn); } } addTemplateString(key, renderFn, declaredDependencies = [], symbolParseFn, templateInstance) { this.add(key, renderFn, declaredDependencies, symbolParseFn, templateInstance); this.templateStringKeyLookup[key] = true; } async resolveVarOrder(data) { let proxyByTemplateString = new ComputedDataTemplateString(this.computedKeys); let proxyByProxy = new ComputedDataProxy(this.computedKeys); for (let key of this.computedKeys) { let computed = lodashGet(this.computed, key); if (typeof computed !== "function") { // add nodes for non functions (primitives like booleans, etc) // This will not handle template strings, as they are normalized to functions this.queue.addNode(key); } else { this.queue.uses(key, this.declaredDependencies[key]); let symbolParseFn = lodashGet(this.symbolParseFunctions, key); let varsUsed = []; if (symbolParseFn) { // use the parseForSymbols function in the TemplateEngine varsUsed = symbolParseFn(); } else if (symbolParseFn !== false) { // skip resolution is this is false (just use declaredDependencies) let isTemplateString = !!this.templateStringKeyLookup[key]; let proxy = isTemplateString ? proxyByTemplateString : proxyByProxy; varsUsed = await proxy.findVarsUsed(computed, data); } debug("%o accesses %o variables", key, varsUsed); let filteredVarsUsed = varsUsed.filter((varUsed) => { return ( (varUsed !== key && this.computedKeys.has(varUsed)) || varUsed.startsWith("collections.") ); }); this.queue.uses(key, filteredVarsUsed); } } } async _setupDataEntry(data, order) { debug("Computed data order of execution: %o", order); for (let key of order) { let computed = lodashGet(this.computed, key); if (typeof computed === "function") { let ret = await computed(data); lodashSet(data, key, ret); } else if (computed !== undefined) { lodashSet(data, key, computed); } } } async setupData(data, orderFilter) { await this.resolveVarOrder(data); await this.processRemainingData(data, orderFilter); } async processRemainingData(data, orderFilter) { // process all variables let order = this.queue.getOrder(); if (orderFilter && typeof orderFilter === "function") { order = order.filter(orderFilter.bind(this.queue)); } await this._setupDataEntry(data, order); this.queue.markComputed(order); } } export default ComputedData; ================================================ FILE: src/Data/ComputedDataProxy.js ================================================ import lodash from "@11ty/lodash-custom"; import { isPlainObject } from "@11ty/eleventy-utils"; const { set: lodashSet, get: lodashGet } = lodash; /* Calculates computed data using Proxies */ class ComputedDataProxy { constructor(computedKeys) { if (Array.isArray(computedKeys)) { this.computedKeys = new Set(computedKeys); } else { this.computedKeys = computedKeys; } } isArrayOrPlainObject(data) { return Array.isArray(data) || isPlainObject(data); } getProxyData(data, keyRef) { // WARNING: SIDE EFFECTS // Set defaults for keys not already set on parent data // TODO should make another effort to get rid of this, // See the ProxyWrap util for more proxy handlers that will likely fix this let undefinedValue = "__11TY_UNDEFINED__"; if (this.computedKeys) { for (let key of this.computedKeys) { if (lodashGet(data, key, undefinedValue) === undefinedValue) { lodashSet(data, key, ""); } } } let proxyData = this._getProxyData(data, keyRef); return proxyData; } _getProxyForObject(dataObj, keyRef, parentKey = "") { return new Proxy( {}, { get: (obj, key) => { if (typeof key !== "string") { return obj[key]; } let newKey = `${parentKey ? `${parentKey}.` : ""}${key}`; // Issue #1137 // Special case for Collections, always return an Array for collection keys // so they it works fine with Array methods like `filter`, `map`, etc if (newKey === "collections") { keyRef.add(newKey); return new Proxy( {}, { get: (target, key) => { if (typeof key === "string") { keyRef.add(`collections.${key}`); return []; } return target[key]; }, }, ); } let newData = this._getProxyData(dataObj[key], keyRef, newKey); if (!this.isArrayOrPlainObject(newData)) { keyRef.add(newKey); } return newData; }, }, ); } _getProxyForArray(dataArr, keyRef, parentKey = "") { return new Proxy(new Array(dataArr.length), { get: (obj, key) => { if (Array.prototype.hasOwnProperty(key)) { // remove `filter`, `constructor`, `map`, etc keyRef.add(parentKey); return obj[key]; } // Hm, this needs to be better if (key === "then") { keyRef.add(parentKey); return; } let newKey = `${parentKey}[${key}]`; let newData = this._getProxyData(dataArr[key], keyRef, newKey); if (!this.isArrayOrPlainObject(newData)) { keyRef.add(newKey); } return newData; }, }); } _getProxyData(data, keyRef, parentKey = "") { if (isPlainObject(data)) { return this._getProxyForObject(data, keyRef, parentKey); } else if (Array.isArray(data)) { return this._getProxyForArray(data, keyRef, parentKey); } // everything else! return data; } async findVarsUsed(fn, data = {}) { let keyRef = new Set(); // careful, logging proxyData will mess with test results! let proxyData = this.getProxyData(data, keyRef); // squelch console logs for this fake proxy data pass 😅 // let savedLog = console.log; // console.log = () => {}; await fn(proxyData); // console.log = savedLog; return Array.from(keyRef); } } export default ComputedDataProxy; ================================================ FILE: src/Data/ComputedDataQueue.js ================================================ import { DepGraph as DependencyGraph } from "dependency-graph"; /* Keeps track of the dependency graph between computed data variables * Removes keys from the graph when they are computed. */ class ComputedDataQueue { constructor() { this.graph = new DependencyGraph(); } getOrder() { return this.graph.overallOrder(); } getOrderFor(name) { return this.graph.dependenciesOf(name); } getDependsOn(name) { return this.graph.dependantsOf(name); } isUsesStartsWith(name, prefix) { if (name.startsWith(prefix)) { return true; } return ( this.graph.dependenciesOf(name).filter((entry) => { return entry.startsWith(prefix); }).length > 0 ); } addNode(name) { if (!this.graph.hasNode(name)) { this.graph.addNode(name); } } _uses(graph, name, varsUsed = []) { if (!graph.hasNode(name)) { graph.addNode(name); } for (let varUsed of varsUsed) { if (!graph.hasNode(varUsed)) { graph.addNode(varUsed); } graph.addDependency(name, varUsed); } } uses(name, varsUsed = []) { this._uses(this.graph, name, varsUsed); } markComputed(varsComputed = []) { for (let varComputed of varsComputed) { this.graph.removeNode(varComputed); } } } export default ComputedDataQueue; ================================================ FILE: src/Data/ComputedDataTemplateString.js ================================================ import lodash from "@11ty/lodash-custom"; import debugUtil from "debug"; const { set: lodashSet } = lodash; const debug = debugUtil("Eleventy:ComputedDataTemplateString"); /* Calculates computed data in Template Strings. * Ideally we would use the Proxy approach but it doesn’t work * in some template languages that visit all available data even if * it isn’t used in the template (Nunjucks) */ class ComputedDataTemplateString { constructor(computedKeys) { if (Array.isArray(computedKeys)) { this.computedKeys = new Set(computedKeys); } else { this.computedKeys = computedKeys; } // is this ¯\_(lisp)_/¯ // must be strings that won’t be escaped by template languages this.prefix = "(((11ty((("; this.suffix = ")))11ty)))"; } getProxyData() { let proxyData = {}; // use these special strings as a workaround to check the rendered output // can’t use proxies here as some template languages trigger proxy for all // keys in data for (let key of this.computedKeys) { // TODO don’t allow to set eleventyComputed.page? other disallowed computed things? lodashSet(proxyData, key, this.prefix + key + this.suffix); } return proxyData; } findVarsInOutput(output = "") { let vars = new Set(); let splits = output.split(this.prefix); for (let split of splits) { let varName = split.slice(0, split.indexOf(this.suffix) < 0 ? 0 : split.indexOf(this.suffix)); if (varName) { vars.add(varName); } } return Array.from(vars); } async findVarsUsed(fn) { let proxyData = this.getProxyData(); let output; // Mitigation for #1061, errors with filters in the first pass shouldn’t fail the whole thing. try { output = await fn(proxyData); } catch (e) { debug("Computed Data first pass data resolution error: %o", e); } // page.outputPath on serverless urls returns false. if (typeof output === "string") { return this.findVarsInOutput(output); } return []; } } export default ComputedDataTemplateString; ================================================ FILE: src/Data/TemplateData.js ================================================ import path from "node:path"; import lodash from "@11ty/lodash-custom"; import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import { inspect } from "../Adapters/Packages/inspect.js"; import unique from "../Util/Objects/Unique.js"; import TemplateGlob from "../TemplateGlob.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import TemplateDataInitialGlobalData from "./TemplateDataInitialGlobalData.js"; import { getEleventyPackageJson, getWorkingProjectPackageJson } from "../Util/ImportJsonSync.js"; import { EleventyImport, EleventyLoadContent } from "../Util/Require.js"; import { DeepFreeze } from "../Util/Objects/DeepFreeze.js"; import { coerce } from "../Util/SemverCoerce.js"; import ReservedData from "../Util/ReservedData.js"; import { isTypeScriptSupported } from "../Util/FeatureTests.cjs"; const { set: lodashSet, get: lodashGet } = lodash; const debugWarn = debugUtil("Eleventy:Warnings"); const debug = debugUtil("Eleventy:TemplateData"); const debugDev = debugUtil("Dev:Eleventy:TemplateData"); class TemplateDataParseError extends EleventyBaseError {} class TemplateData { constructor(templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new Error( "Internal error: Missing `templateConfig` or was not an instance of `TemplateConfig`.", ); } this.templateConfig = templateConfig; this.config = this.templateConfig.getConfig(); this.benchmarks = { data: this.config.benchmarkManager.get("Data"), aggregate: this.config.benchmarkManager.get("Aggregate"), }; this.rawImports = {}; this.globalData = null; this.templateDirectoryData = {}; this.isEsm = false; this.initialGlobalData = new TemplateDataInitialGlobalData(this.templateConfig); } get dirs() { return this.templateConfig.directories; } get inputDir() { return this.dirs.input; } // if this was set but `falsy` we would fallback to inputDir get dataDir() { return this.dirs.data; } get absoluteDataDir() { return TemplatePath.absolutePath(this.dataDir); } // This was async in 2.0 and prior but doesn’t need to be any more. getInputDir() { return this.dirs.input; } getDataDir() { return this.dataDir; } exists(pathname) { // It's common for data files not to exist, so we avoid going to the FS to // re-check if they do via a quick-and-dirty cache. return this.templateConfig.existsCache.exists(pathname); } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } setProjectUsingEsm(isEsmProject) { this.isEsm = !!isEsmProject; } get extensionMap() { if (!this._extensionMap) { throw new Error("Internal error: missing `extensionMap` in TemplateData."); } return this._extensionMap; } set extensionMap(map) { this._extensionMap = map; } get environmentVariables() { return this._env; } set environmentVariables(env) { this._env = env; } /* Used by tests */ _setConfig(config) { this.config = config; } getRawImports() { if (!this.config.keys.package) { debug( "Opted-out of package.json assignment for global data with falsy value for `keys.package` configuration.", ); return this.rawImports; } else if (Object.keys(this.rawImports).length > 0) { return this.rawImports; } let pkgJson = getWorkingProjectPackageJson(); this.rawImports[this.config.keys.package] = pkgJson; if (this.config.freezeReservedData) { DeepFreeze(this.rawImports); } return this.rawImports; } clearData() { this.globalData = null; this.configApiGlobalData = null; this.templateDirectoryData = {}; } _getGlobalDataGlobByExtension(extension) { return TemplateGlob.normalizePath(this.dataDir, `/**/*.${extension}`); } // This is a backwards compatibility helper with the old `jsDataFileSuffix` configuration API getDataFileSuffixes() { // New API if (Array.isArray(this.config.dataFileSuffixes)) { return this.config.dataFileSuffixes; } // Backwards compatibility if (this.config.jsDataFileSuffix) { let suffixes = []; suffixes.push(this.config.jsDataFileSuffix); // e.g. filename.11tydata.json suffixes.push(""); // suffix-less for free with old API, e.g. filename.json return suffixes; } return []; // if both of these entries are set to false, use no files } // This is used exclusively for --watch and --serve chokidar targets async getTemplateDataFileGlob() { let suffixes = this.getDataFileSuffixes(); let globSuffixesWithLeadingDot = new Set(); globSuffixesWithLeadingDot.add("json"); // covers .11tydata.json too let globSuffixesWithoutLeadingDot = new Set(); // Typically using [ '.11tydata', '' ] suffixes to find data files for (let suffix of suffixes) { // TODO the `suffix` truthiness check is purely for backwards compat? if (suffix && typeof suffix === "string") { if (suffix.startsWith(".")) { // .suffix.js globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.mjs`); globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.cjs`); globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.js`); if (isTypeScriptSupported()) { globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.mts`); globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.cts`); globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.ts`); } } else { // "suffix.js" without leading dot globSuffixesWithoutLeadingDot.add(`${suffix || ""}.mjs`); globSuffixesWithoutLeadingDot.add(`${suffix || ""}.cjs`); globSuffixesWithoutLeadingDot.add(`${suffix || ""}.js`); if (isTypeScriptSupported()) { globSuffixesWithoutLeadingDot.add(`${suffix || ""}.mts`); globSuffixesWithoutLeadingDot.add(`${suffix || ""}.cts`); globSuffixesWithoutLeadingDot.add(`${suffix || ""}.ts`); } } } } // Configuration Data Extensions e.g. yaml if (this.hasUserDataExtensions()) { for (let extension of this.getUserDataExtensions()) { globSuffixesWithLeadingDot.add(extension); // covers .11tydata.{extension} too } } let paths = []; if (globSuffixesWithLeadingDot.size > 0) { paths.push(`${this.inputDir}**/*.{${Array.from(globSuffixesWithLeadingDot).join(",")}}`); } if (globSuffixesWithoutLeadingDot.size > 0) { paths.push(`${this.inputDir}**/*{${Array.from(globSuffixesWithoutLeadingDot).join(",")}}`); } return TemplatePath.addLeadingDotSlashArray(paths); } // For spidering dependencies // TODO Can we reuse getTemplateDataFileGlob instead? Maybe just filter off the .json files before scanning for dependencies getTemplateJavaScriptDataFileGlob() { let paths = []; let suffixes = this.getDataFileSuffixes(); for (let suffix of suffixes) { if (suffix) { // TODO this check is purely for backwards compat and I kinda feel like it shouldn’t be here // paths.push(`${this.inputDir}/**/*${suffix || ""}.cjs`); // Same as above paths.push(`${this.inputDir}**/*${suffix || ""}.js`); // TODO typescript? } } return TemplatePath.addLeadingDotSlashArray(paths); } getGlobalDataGlob() { let extGlob = this.getGlobalDataExtensionPriorities().join(","); return [this._getGlobalDataGlobByExtension("{" + extGlob + "}")]; } getWatchPathCache() { return this.pathCache; } getGlobalDataExtensionPriorities() { return this.getUserDataExtensions().concat(["json", "mjs", "cjs", "js"]); } static calculateExtensionPriority(path, priorities) { for (let i = 0; i < priorities.length; i++) { let ext = priorities[i]; if (path.endsWith(ext)) { return i; } } return priorities.length; } async getGlobalDataFiles() { let priorities = this.getGlobalDataExtensionPriorities(); let fsBench = this.benchmarks.aggregate.get("Searching the file system (data)"); fsBench.before(); let globs = this.getGlobalDataGlob(); let paths = []; if (this.fileSystemSearch) { paths = await this.fileSystemSearch.search("global-data", globs); } fsBench.after(); // sort paths according to extension priorities // here we use reverse ordering, because paths with bigger index in array will override the first ones // example [path/file.json, path/file.js] here js will override json paths = paths.sort((first, second) => { let p1 = TemplateData.calculateExtensionPriority(first, priorities); let p2 = TemplateData.calculateExtensionPriority(second, priorities); if (p1 < p2) { return -1; } if (p1 > p2) { return 1; } return 0; }); this.pathCache = paths; return paths; } getObjectPathForDataFile(dataFilePath) { let absoluteDataFilePath = TemplatePath.absolutePath(dataFilePath); let reducedPath = TemplatePath.stripLeadingSubPath(absoluteDataFilePath, this.absoluteDataDir); let parsed = path.parse(reducedPath); let folders = parsed.dir ? parsed.dir.split("/") : []; folders.push(parsed.name); return folders; } async getAllGlobalData() { let globalData = {}; let files = TemplatePath.addLeadingDotSlashArray(await this.getGlobalDataFiles()); this.config.events.emit("eleventy.globalDataFiles", files); let dataFileConflicts = {}; for (let j = 0, k = files.length; j < k; j++) { let data = await this.getDataValue(files[j]); let objectPathTarget = this.getObjectPathForDataFile(files[j]); // Since we're joining directory paths and an array is not usable as an objectkey since two identical arrays are not double equal, // we can just join the array by a forbidden character ("/"" is chosen here, since it works on Linux, Mac and Windows). // If at some point this isn't enough anymore, it would be possible to just use JSON.stringify(objectPathTarget) since that // is guaranteed to work but is signifivcantly slower. let objectPathTargetString = objectPathTarget.join(path.sep); // if two global files have the same path (but different extensions) // and conflict, let’s merge them. if (dataFileConflicts[objectPathTargetString]) { debugWarn( `merging global data from ${files[j]} with an already existing global data file (${dataFileConflicts[objectPathTargetString]}). Overriding existing keys.`, ); let oldData = lodashGet(globalData, objectPathTarget); data = Merge(oldData, data); } dataFileConflicts[objectPathTargetString] = files[j]; debug(`Found global data file ${files[j]} and adding as: ${objectPathTarget}`); lodashSet(globalData, objectPathTarget, data); if (this.config.freezeReservedData) { ReservedData.check(globalData, files[j]); } } return globalData; } async #getInitialGlobalData() { let globalData = await this.initialGlobalData.getData(); if (!("eleventy" in globalData)) { globalData.eleventy = {}; } // #2293 for meta[name=generator] const pkg = getEleventyPackageJson(); globalData.eleventy.version = coerce(pkg.version).toString(); globalData.eleventy.generator = `Eleventy v${globalData.eleventy.version}`; if (this.environmentVariables) { if (!("env" in globalData.eleventy)) { globalData.eleventy.env = {}; } Object.assign(globalData.eleventy.env, this.environmentVariables); } if (this.dirs) { if (!("directories" in globalData.eleventy)) { globalData.eleventy.directories = {}; } Object.assign(globalData.eleventy.directories, this.dirs.getUserspaceInstance()); } // Reserved if (this.config.freezeReservedData) { DeepFreeze(globalData.eleventy); } return globalData; } async getInitialGlobalData() { if (!this.configApiGlobalData) { this.configApiGlobalData = this.#getInitialGlobalData(); } return this.configApiGlobalData; } async #getGlobalData() { let rawImports = this.getRawImports(); let configApiGlobalData = await this.getInitialGlobalData(); let globalJson = await this.getAllGlobalData(); let mergedGlobalData = Merge(globalJson, configApiGlobalData); // OK: Shallow merge when combining rawImports (pkg) with global data files return Object.assign({}, mergedGlobalData, rawImports); } async getGlobalData() { if (!this.globalData) { this.globalData = this.#getGlobalData(); } return this.globalData; } /* Template and Directory data files */ async combineLocalData(localDataPaths) { let localData = {}; if (!Array.isArray(localDataPaths)) { localDataPaths = [localDataPaths]; } // Filter out files we know don't exist to avoid overhead for checking localDataPaths = localDataPaths.filter((path) => { return this.exists(path); }); this.config.events.emit("eleventy.dataFiles", localDataPaths); if (!localDataPaths.length) { return localData; } let dataSource = {}; for (let path of localDataPaths) { let dataForPath = await this.getDataValue(path); if (!isPlainObject(dataForPath)) { debug( "Warning: Template and Directory data files expect an object to be returned, instead `%o` returned `%o`", path, dataForPath, ); } else { // clean up data for template/directory data files only. let cleanedDataForPath = TemplateData.cleanupData(dataForPath, { file: path, }); for (let key in cleanedDataForPath) { if (Object.prototype.hasOwnProperty.call(dataSource, key)) { debugWarn( "Local data files have conflicting data. Overwriting '%s' with data from '%s'. Previous data location was from '%s'", key, path, dataSource[key], ); } dataSource[key] = path; } Merge(localData, cleanedDataForPath); } } return localData; } async getTemplateDirectoryData(templatePath) { if (!this.templateDirectoryData[templatePath]) { let localDataPaths = await this.getLocalDataPaths(templatePath); let importedData = await this.combineLocalData(localDataPaths); this.templateDirectoryData[templatePath] = importedData; } return this.templateDirectoryData[templatePath]; } getUserDataExtensions() { if (!this.config.dataExtensions) { return []; } // returning extensions in reverse order to create proper extension order // later added formats will override first ones return Array.from(this.config.dataExtensions.keys()).reverse(); } getUserDataParser(extension) { return this.config.dataExtensions.get(extension); } isUserDataExtension(extension) { return this.config.dataExtensions && this.config.dataExtensions.has(extension); } hasUserDataExtensions() { return this.config.dataExtensions && this.config.dataExtensions.size > 0; } async _parseDataFile(path, parser, options = {}) { let readFile = !("read" in options) || options.read === true; let rawInput; if (readFile) { rawInput = EleventyLoadContent(path, options); } if (readFile && !rawInput) { return {}; } try { if (readFile) { return parser(rawInput, path); } else { // path as a first argument is when `read: false` // path as a second argument is for consistency with `read: true` API return parser(path, path); } } catch (e) { throw new TemplateDataParseError(`Having trouble parsing data file ${path}`, e); } } // ignoreProcessing = false for global data files // ignoreProcessing = true for local data files async getDataValue(path) { let extension = TemplatePath.getExtension(path); if (extension === "js" || extension === "cjs" || extension === "mjs") { // JS data file or require’d JSON (no preprocessing needed) if (!this.exists(path)) { return {}; } let aggregateDataBench = this.benchmarks.aggregate.get("Data File"); aggregateDataBench.before(); let dataBench = this.benchmarks.data.get(`\`${path}\``); dataBench.before(); let type = "cjs"; if (extension === "mjs" || (extension === "js" && this.isEsm)) { type = "esm"; } // We always need to use `import()`, as `require` isn’t available in ESM. let returnValue = await EleventyImport(path, type); // TODO special exception for Global data `permalink.js` // module.exports = (data) => `${data.page.filePathStem}/`; // Does not work // module.exports = () => ((data) => `${data.page.filePathStem}/`); // Works if (typeof returnValue === "function") { let configApiGlobalData = await this.getInitialGlobalData(); returnValue = await returnValue(configApiGlobalData || {}); } dataBench.after(); aggregateDataBench.after(); return returnValue; } else if (this.isUserDataExtension(extension)) { // Other extensions let { parser, options } = this.getUserDataParser(extension); return this._parseDataFile(path, parser, options); } else if (extension === "json") { // File to string, parse with JSON (preprocess) const parser = (content) => JSON.parse(content); return this._parseDataFile(path, parser); } else { throw new TemplateDataParseError( `Could not find an appropriate data parser for ${path}. Do you need to add a plugin to your config file?`, ); } } _pushExtensionsToPaths(paths, curpath, extensions) { for (let extension of extensions) { paths.push(curpath + "." + extension); } } _addBaseToPaths(paths, base, extensions, nonEmptySuffixesOnly = false) { let suffixes = this.getDataFileSuffixes(); for (let suffix of suffixes) { suffix = suffix || ""; if (nonEmptySuffixesOnly && suffix === "") { continue; } // data suffix if (suffix) { paths.push(base + suffix + ".js"); paths.push(base + suffix + ".cjs"); paths.push(base + suffix + ".mjs"); } paths.push(base + suffix + ".json"); // default: .11tydata.json // inject user extensions this._pushExtensionsToPaths(paths, base + suffix, extensions); } } async getLocalDataPaths(templatePath) { let paths = []; let parsed = path.parse(templatePath); let inputDir = this.inputDir; debugDev("getLocalDataPaths(%o)", templatePath); debugDev("parsed.dir: %o", parsed.dir); let userExtensions = this.getUserDataExtensions(); if (parsed.dir) { let fileNameNoExt = this.extensionMap.removeTemplateExtension(parsed.base); // default dataSuffix: .11tydata, is appended in _addBaseToPaths debug("Using %o suffixes to find data files.", this.getDataFileSuffixes()); // Template data file paths let filePathNoExt = parsed.dir + "/" + fileNameNoExt; this._addBaseToPaths(paths, filePathNoExt, userExtensions); // Directory data file paths let allDirs = TemplatePath.getAllDirs(parsed.dir); debugDev("allDirs: %o", allDirs); for (let dir of allDirs) { let lastDir = TemplatePath.getLastPathSegment(dir); let dirPathNoExt = dir + "/" + lastDir; if (inputDir) { debugDev("dirStr: %o; inputDir: %o", dir, inputDir); } // TODO use DirContains if (!inputDir || (dir.startsWith(inputDir) && dir !== inputDir)) { if (this.config.dataFileDirBaseNameOverride) { let indexDataFile = dir + "/" + this.config.dataFileDirBaseNameOverride; this._addBaseToPaths(paths, indexDataFile, userExtensions, true); } else { this._addBaseToPaths(paths, dirPathNoExt, userExtensions); } } } // 0.11.0+ include root input dir files // if using `docs/` as input dir, looks for docs/docs.json et al if (inputDir) { let lastInputDir = TemplatePath.addLeadingDotSlash( TemplatePath.join(inputDir, TemplatePath.getLastPathSegment(inputDir)), ); // in root input dir, search for index.11tydata.json et al if (this.config.dataFileDirBaseNameOverride) { let indexDataFile = TemplatePath.getDirFromFilePath(lastInputDir) + "/" + this.config.dataFileDirBaseNameOverride; this._addBaseToPaths(paths, indexDataFile, userExtensions, true); } else if (lastInputDir !== "./") { this._addBaseToPaths(paths, lastInputDir, userExtensions); } } } debug("getLocalDataPaths(%o): %o", templatePath, paths); return unique(paths).reverse(); } /* Like cleanupData() but does not mutate */ static getCleanedTagsImmutable(data, options = {}) { let tags = []; if (isPlainObject(data) && data.tags) { if (typeof data.tags === "string") { tags = (data.tags || "").split(","); } else if (Array.isArray(data.tags)) { tags = data.tags; } else if (data.tags) { throw new Error( `String or Array expected for \`tags\`${options.file ? ` in ${options.isVirtualTemplate ? "virtual " : ""}template: ${options.file}` : ""}. Received: ${inspect(data.tags)}`, ); } // Deduplicate tags // Coerce to string #3875 return [...new Set(tags)].map((entry) => String(entry)); } return tags; } static cleanupData(data, options = {}) { if (isPlainObject(data) && "tags" in data) { data.tags = this.getCleanedTagsImmutable(data, options); } return data; } static getNormalizedExcludedCollections(data) { let excludes = []; let key = "eleventyExcludeFromCollections"; if (data?.[key] !== true) { if (Array.isArray(data[key])) { excludes = data[key]; } else if (typeof data[key] === "string") { excludes = (data[key] || "").split(","); } } return { excludes, excludeAll: data?.eleventyExcludeFromCollections === true, }; } static getIncludedCollectionNames(data) { let tags = TemplateData.getCleanedTagsImmutable(data); let { excludes, excludeAll } = TemplateData.getNormalizedExcludedCollections(data); if (excludeAll) { return []; } return ["all", ...tags].filter((tag) => !excludes.includes(tag)); } static getIncludedTagNames(data) { return this.getIncludedCollectionNames(data).filter((tagName) => tagName !== "all"); } } export default TemplateData; ================================================ FILE: src/Data/TemplateDataInitialGlobalData.js ================================================ import lodash from "@11ty/lodash-custom"; import ReservedData from "../Util/ReservedData.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; const { set: lodashSet } = lodash; class TemplateDataConfigError extends EleventyBaseError {} class TemplateDataInitialGlobalData { constructor(templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new TemplateDataConfigError("Missing or invalid `templateConfig` (via Render plugin)."); } this.templateConfig = templateConfig; this.config = this.templateConfig.getConfig(); } async getData() { let globalData = {}; // via eleventyConfig.addGlobalData if (this.config.globalData) { let keys = Object.keys(this.config.globalData); for (let key of keys) { let returnValue = this.config.globalData[key]; // This section is problematic when used with eleventyComputed #3389 if (typeof returnValue === "function") { returnValue = await returnValue(); } lodashSet(globalData, key, returnValue); } } if (this.config.freezeReservedData) { // TODO-ish might come from the `config` callback too ReservedData.check(globalData, this.templateConfig.getActiveConfigPath()); } return globalData; } } export default TemplateDataInitialGlobalData; ================================================ FILE: src/Eleventy.js ================================================ import { relative } from "node:path"; import debugUtil from "debug"; import { TemplatePath } from "@11ty/eleventy-utils"; import { Core } from "./Core.js"; import EleventyServe from "./EleventyServe.js"; import { Watch } from "./Watch.js"; import WatchQueue from "./WatchQueue.js"; import WatchTargets from "./WatchTargets.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; // Utils import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js"; import PathPrefixer from "./Util/PathPrefixer.js"; import PathNormalizer from "./Util/PathNormalizer.js"; import { isGlobMatch } from "./Util/GlobMatcher.js"; import eventBus from "./EventBus.js"; import { withResolvers } from "./Util/PromiseUtil.js"; const debug = debugUtil("Eleventy"); export default class Eleventy extends Core { /** @type {boolean} */ #isStopping = false; /** @type {WatchQueue} */ #watchQueue; // constructor(input, output, options = {}, eleventyConfig = null) { // super(input, output, options, eleventyConfig); // } /** * Sets the incremental build mode. * * @param {boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates */ setIncrementalBuild(isIncremental) { super.setIncrementalBuild(isIncremental); if (this.#watchQueue) { this.watchQueue.incremental = !!isIncremental; } } get watchQueue() { if (!this.#watchQueue) { this.#watchQueue = new WatchQueue(); this.#watchQueue.incremental = this.isIncremental; } return this.#watchQueue; } async initializeConfig(initOverrides) { await super.initializeConfig(initOverrides); // Careful to make sure the previous server closes on SIGINT, issue #3873 if (!this.eleventyServe) { /** @type {object} */ this.eleventyServe = new EleventyServe(); } this.eleventyServe.eleventyConfig = this.eleventyConfig; /** @type {object} */ this.watchTargets = new WatchTargets(this.eleventyConfig); this.watchTargets.add(this.config.additionalWatchTargets); } async resetConfig() { await super.resetConfig(); // TODO set this.eleventyServe with this.getChokidarConfig() if (checkPassthroughCopyBehavior(this.config, this.runMode)) { this.eleventyServe.resetConfig(); } } /** * Starts Eleventy. */ async init(options = {}) { await super.init(options); // eleventyServe is always available, even when not in --serve mode // TODO directorynorm this.eleventyServe.setOutputDir(this.outputDir); if (checkPassthroughCopyBehavior(this.config, this.runMode)) { this.eleventyServe.watchPassthroughCopy( this.eleventyFiles.getGlobWatcherFilesForPassthroughCopy(), ); } } /** * @param {string} changedFilePath - File that triggered a re-run (added or modified) * @param {boolean} [isResetConfig] - are we doing a config reset */ async #addFileToWatchQueue(changedFilePath, isResetConfig) { // Currently this is only for 11ty.js deps but should be extended with usesGraph let usedByDependants = []; if (this.watchTargets) { usedByDependants = this.watchTargets.getDependantsOf( TemplatePath.addLeadingDotSlash(changedFilePath), ); } let relevantLayouts = this.eleventyConfig.usesGraph.getLayoutsUsedBy(changedFilePath); // `eleventy.templateModified` is no longer used internally, remove in a future major version. eventBus.emit("eleventy.templateModified", changedFilePath, { usedByDependants, relevantLayouts, }); // These listeners are *global*, not cleared even on config reset eventBus.emit("eleventy.resourceModified", changedFilePath, usedByDependants, { viaConfigReset: isResetConfig, relevantLayouts, }); this.config.events.emit("eleventy#templateModified", changedFilePath); this.watchQueue.addToPendingQueue(changedFilePath); } shouldTriggerConfigReset(changedFiles) { // looks for all eligible config files (not just the active one, handles config file rename) let configFilePaths = new Set(this.eleventyConfig.getLocalProjectConfigFiles()); // https://www.11ty.dev/docs/watch-serve/#reset-configuration let resetConfigGlobs = WatchTargets.normalizeToGlobs( Array.from(this.eleventyConfig.userConfig.watchTargetsConfigReset), ); for (let filePath of changedFiles) { if (configFilePaths.has(filePath)) { return true; } if (isGlobMatch(filePath, resetConfigGlobs)) { return true; } } for (let configFilePath of configFilePaths) { // Any dependencies of the config file changed let configFileDependencies = new Set(this.watchTargets.getDependenciesOf(configFilePath)); for (let filePath of changedFiles) { if (configFileDependencies.has(filePath)) { return true; } } } return false; } // Checks the build queue to see if any configuration related files have changed #shouldResetConfig(activeQueue = []) { if (!activeQueue.length) { return false; } return this.shouldTriggerConfigReset( activeQueue.map((path) => { return PathNormalizer.normalizeSeperator(TemplatePath.addLeadingDotSlash(path)); }), ); } async #rewatch(isResetConfig = false) { if (this.watchQueue.isBuildRunning()) { return; } this.watchQueue.setBuildRunning(); let queue = this.watchQueue.getActiveQueue(); await this.config.events.emit("beforeWatch", queue); await this.config.events.emit("eleventy.beforeWatch", queue); // Clear `import` cache for all files that triggered the rebuild (sync event) this.watchTargets.clearImportCacheFor(queue); // reset and reload global configuration if (isResetConfig) { // important: run this before config resets otherwise the handlers will disappear. await this.config.events.emit("eleventy.reset"); this.resetConfig(); } await this.restart(); await this.init({ viaConfigReset: isResetConfig }); try { let [passthroughCopyResults, templateResults] = await this.write(); if (isResetConfig) { // make sure this happens after write() await this.startWatch(); } this.watchTargets.reset(); await this.#initWatchDependencies(); // Add new deps to chokidar let newWatchTargets = this.watchTargets.getNewTargetsSinceLastReset(); this.watcher.watchTargets(newWatchTargets); // Is a CSS input file and is not in the includes folder // TODO check output path file extension of this template (not input path) // TODO add additional API for this, maybe a config callback? let onlyCssChanges = this.watchQueue.hasAllQueueFiles((path) => { return ( path.endsWith(".css") && // TODO how to make this work with relative includes? !TemplatePath.startsWithSubPath(path, this.eleventyFiles.getIncludesDir()) ); }); let files = this.watchQueue.getActiveQueue(); // Maps passthrough copy files to output URLs for CSS live reload let stylesheetUrls = new Set(); for (let entry of passthroughCopyResults) { for (let filepath in entry.map) { if ( filepath.endsWith(".css") && files.includes(TemplatePath.addLeadingDotSlash(filepath)) ) { stylesheetUrls.add( "/" + TemplatePath.stripLeadingSubPath(entry.map[filepath], this.outputDir), ); } } } let normalizedPathPrefix = PathPrefixer.normalizePathPrefix(this.config.pathPrefix); let matchingTemplates = templateResults .flat() .filter((entry) => Boolean(entry)) .map((entry) => { // only `url`, `inputPath`, and `content` are used: https://github.com/11ty/eleventy-dev-server/blob/1c658605f75224fdc76f68aebe7a412eeb4f1bc9/client/reload-client.js#L140 entry.url = PathPrefixer.joinUrlParts(normalizedPathPrefix, entry.url); delete entry.rawInput; // Issue #3481 return entry; }); await this.eleventyServe.reload({ files, subtype: onlyCssChanges ? "css" : undefined, build: { stylesheets: Array.from(stylesheetUrls), templates: matchingTemplates, }, }); } catch (error) { this.eleventyServe.sendError({ error, }); } this.watchQueue.setBuildFinished(); let queueSize = this.watchQueue.getPendingQueueSize(); if (queueSize > 0) { this.logger.log( `You saved while Eleventy was running, let’s run again. (${queueSize} change${ queueSize !== 1 ? "s" : "" })`, ); await this.#rewatch(); } } /** * @returns {module:11ty/eleventy/src/Benchmark/BenchmarkGroup~BenchmarkGroup} */ get watcherBench() { return this.bench.get("Watcher"); } /** * Set up watchers and benchmarks. * * @async * @method */ async startWatch() { if (this.projectPackageJsonPath) { this.watchTargets.add([relative(TemplatePath.getWorkingDir(), this.projectPackageJsonPath)]); } this.watchTargets.add(this.eleventyFiles.getGlobWatcherFiles()); this.watchTargets.add(this.eleventyFiles.getIgnoreFiles()); // Watch the local project config file this.watchTargets.add(this.eleventyConfig.getActiveConfigPath()); // Template and Directory Data Files this.watchTargets.add(await this.eleventyFiles.getGlobWatcherTemplateDataFiles()); let benchmark = this.watcherBench.get( "Watching JavaScript Dependencies (disable with `eleventyConfig.setWatchJavaScriptDependencies(false)`)", ); benchmark.before(); await this.#initWatchDependencies(); benchmark.after(); // Close previous watcher if (this.watcher) { await this.watcher.close(); } // TODO improve unwatching if JS dependencies are removed (or files are deleted) let { targets, ignores } = await this.getWatchedTargets(); debug("Watching for changes to: %o", targets); this.watcher = new Watch(this.eleventyConfig); this.watcher.watchTargets(targets); this.watcher.addIgnores(ignores); await this.watcher.start(); this.logger.forceLog("Watching…"); let watchDelay; let watchRun = async (path) => { path = TemplatePath.normalize(path); try { let isResetConfig = this.#shouldResetConfig([path]); this.#addFileToWatchQueue(path, isResetConfig); clearTimeout(watchDelay); let { promise, resolve, reject } = withResolvers(); watchDelay = setTimeout(async () => { this.#rewatch(isResetConfig).then(resolve, reject); }, this.config.watchThrottleWaitTime); await promise; } catch (e) { if (e instanceof EleventyBaseError) { this.errorHandler.error(e, "Eleventy watch error"); this.watchQueue.setBuildFinished(); } else { this.errorHandler.fatal(e, "Eleventy fatal watch error"); await this.close(); } } this.config.events.emit("eleventy.afterwatch"); }; this.watcher.on("change", async (path) => { // Emulated passthrough copy logs from the server if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) { this.logger.forceLog( `File changed: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`, ); } await watchRun(path); }); this.watcher.on("add", async (path) => { // Emulated passthrough copy logs from the server if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) { this.logger.forceLog( `File added: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`, ); } this.fileSystemSearch.add(path); await watchRun(path); }); this.watcher.on("unlink", async (path) => { // Emulated passthrough copy logs from the server if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) { this.logger.forceLog( `File deleted: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`, ); } this.fileSystemSearch.delete(path); await watchRun(path); }); // For testability return watchRun; } /** * Starts watching dependencies. */ async #initWatchDependencies() { if (!this.eleventyConfig.shouldSpiderJavaScriptDependencies()) { return; } // Lazy resolve isEsm only for --watch this.watchTargets.setProjectUsingEsm(this.isEsm); // Template files .11ty.js let templateFiles = await this.eleventyFiles.getWatchPathCache(); await this.watchTargets.addDependencies(templateFiles); // TODO use DirContains let dataDir = TemplatePath.stripLeadingDotSlash(this.templateData.getDataDir()); function filterOutGlobalDataFiles(path) { return !dataDir || !TemplatePath.stripLeadingDotSlash(path).startsWith(dataDir); } // Config file dependencies await this.watchTargets.addDependencies( this.eleventyConfig.getActiveConfigPath(), filterOutGlobalDataFiles, ); // Deps from Global Data (that aren’t in the global data directory, everything is watched there) let globalDataDeps = this.templateData.getWatchPathCache(); await this.watchTargets.addDependencies(globalDataDeps, filterOutGlobalDataFiles); await this.watchTargets.addDependencies( await this.eleventyFiles.getWatcherTemplateJavaScriptDataFiles(), ); } /** * Returns all watched paths * * @async * @method * @returns {Object} `targets` file paths, and `ignores` globs Array */ async getWatchedTargets() { return { targets: await this.watchTargets.getTargets(), ignores: this.eleventyFiles.getGlobWatcherIgnores(), }; } /** * Start watching files * * @async * @method */ async watch() { this.watcherBench.setMinimumThresholdMs(500); this.watcherBench.reset(); // Note that watching indirectly depends on this for fetching dependencies from JS files // See: TemplateWriter:pathCache and WatchTargets await this.write(); let initWatchBench = this.watcherBench.get("Start up --watch"); initWatchBench.before(); let watchRun = await this.startWatch(); initWatchBench.after(); this.watcherBench.finish("Watch"); // Returns for testability return watchRun; } // Renamed to close() async stopWatch() { return this.close(); } async close() { // Prevent multiple invocations. if (this.#isStopping) { return this.#isStopping; } debug("Cleaning up chokidar and server instances, if they exist."); this.#isStopping = Promise.all([this.eleventyServe.close(), this.watcher?.close()]).then(() => { this.#isStopping = false; }); return this.#isStopping; } /** * Serve Eleventy on this port. * * @param {Number} port - The HTTP port to serve Eleventy from. */ async serve(port) { // Port is optional and in this case likely via --port on the command line // May defer to configuration API options `port` property return this.eleventyServe.serve(port); } /** * Shows a help message including usage. * * @static * @returns {string} - The help message. */ static getHelp() { return `Usage: eleventy eleventy --input=. --output=./_site eleventy --serve Arguments: --version --input=. Input template files (default: \`.\`) --output=_site Write HTML output to this folder (default: \`_site\`) --serve Run web server on --port (default 8080) and watch them too --port Run the --serve web server on this port (default 8080) --watch Wait for files to change and automatically rewrite (no web server) --incremental Only build the files that have changed. Best with watch/serve. --incremental=filename.md Does not require watch/serve. Run an incremental build targeting a single file. --ignore-initial Start without a build; build when files change. Works best with watch/serve/incremental. --formats=liquid,md Allow only certain template types (default: \`*\`) --quiet Don’t print all written files (off by default) --config=filename.js Override the eleventy config file path (default: \`.eleventy.js\`) --pathprefix='/' Change all url template filters to use this subdirectory. --dryrun Don’t write any files. Useful in DEBUG mode, for example: \`DEBUG=Eleventy* npx @11ty/eleventy --dryrun\` --loader Set to "esm" to force ESM mode, "cjs" to force CommonJS mode, or "auto" (default) to infer it from package.json. --to=json Change the output to JSON (default: \`fs\`) --to=fs:templates Writes templates, skips passthrough copy --help`; } /** * @deprecated since 1.0.1, use static Eleventy.getHelp() */ getHelp() { return Eleventy.getHelp(); } /* Removed methods */ initWatch() { throw new Error( "Eleventy#initWatch() was removed in v4. Use Eleventy#startWatch() instead (initializes and starts the watcher)", ); } getWatchedFiles() { throw new Error( "Eleventy#getWatchedFiles() was removed in Eleventy v4. Use Eleventy#getWatchedTargets().targets instead.", ); } } export { Eleventy }; /* Utils */ export { EleventyImport as ImportFile } from "./Util/Require.js"; // TODO(breaking) remove these and recommend folks use package level exports e.g. "@11ty/eleventy/plugins/i18n" /* Plugins */ export { default as BundlePlugin } from "@11ty/eleventy-plugin-bundle"; // Eleventy*Plugin names are legacy names export { default as RenderPlugin, default as EleventyRenderPlugin, } from "./Plugins/RenderPlugin.js"; export { default as I18nPlugin, default as EleventyI18nPlugin } from "./Plugins/I18nPlugin.js"; export { default as HtmlBasePlugin, default as EleventyHtmlBasePlugin, } from "./Plugins/HtmlBasePlugin.js"; export { TransformPlugin as InputPathToUrlTransformPlugin } from "./Plugins/InputPathToUrl.js"; export { IdAttributePlugin } from "./Plugins/IdAttributePlugin.js"; export { PreserveClosingTagsPlugin } from "./Plugins/PreserveClosingTagsPlugin.js"; // Error messages for Removed plugins export function EleventyServerlessBundlerPlugin() { throw new Error( "Following feedback from our Community Survey, low interest in this plugin prompted its removal from Eleventy core in 3.0 as we refocus on static sites. Learn more: https://v3.11ty.dev/docs/plugins/serverless/", ); } export { EleventyServerlessBundlerPlugin as EleventyServerless }; export function EleventyEdgePlugin() { throw new Error( "Following feedback from our Community Survey, low interest in this plugin prompted its removal from Eleventy core in 3.0 as we refocus on static sites. Learn more: https://v3.11ty.dev/docs/plugins/edge/", ); } ================================================ FILE: src/EleventyCommonJs.cjs ================================================ function canRequireModules() { // via --experimental-require-module or newer than Node 22 support when this flag is no longer necessary try { require("./Util/Objects/SampleModule.mjs"); return true; } catch(e) { if(e.code === "ERR_REQUIRE_ESM") { return false; } // Rethrow if not an ESM require error. throw e; } } if(!canRequireModules()) { let error = new Error(`\`require("@11ty/eleventy")\` is incompatible with Eleventy v3 and this version of Node. You have a few options: 1. (Easiest) Change the \`require\` to use a dynamic import inside of an asynchronous CommonJS configuration callback, for example: module.exports = async function { const {RenderPlugin, I18nPlugin, HtmlBasePlugin} = await import("@11ty/eleventy"); } 2. (Easier) Update the JavaScript syntax in your configuration file from CommonJS to ESM (change \`require\` to use \`import\` and rename the file to have an \`.mjs\` file extension). 3. (More work) Change your project to use ESM-first by adding \`"type": "module"\` to your package.json. Any \`.js\` will need to be ported to use ESM syntax (or renamed to \`.cjs\`.) 4. Upgrade your Node version (at time of writing, v20.19 or newer) to enable this behavior. If you use a version of Node older than v20.19, try the --experimental-require-module command line flag in Node. Read more: https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require`); error.skipOriginalStack = true; throw error; } // If we made it here require(ESM) works fine (via --experimental-require-module or newer Node.js defaults) let mod = require("./Eleventy.js"); module.exports = mod; ================================================ FILE: src/EleventyExtensionMap.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import { isTypeScriptSupported } from "./Util/FeatureTests.cjs"; class EleventyExtensionMap { #engineManager; constructor(config) { this.setTemplateConfig(config); this._spiderJsDepsCache = {}; /** @type {Array} */ this.validTemplateLanguageKeys; } setFormats(formatKeys = []) { // raw this.formatKeys = formatKeys; this.unfilteredFormatKeys = formatKeys.map(function (key) { return key.trim().toLowerCase(); }); this.validTemplateLanguageKeys = this.unfilteredFormatKeys.filter((key) => this.hasExtension(key), ); this.passthroughCopyKeys = this.unfilteredFormatKeys.filter((key) => !this.hasExtension(key)); } setTemplateConfig(config) { if (!config || config.constructor.name !== "TemplateConfig") { throw new Error("Internal error: Missing or invalid `config` argument."); } this.templateConfig = config; } get config() { return this.templateConfig.getConfig(); } get engineManager() { if (!this.#engineManager) { throw new Error("Internal error: Missing `#engineManager` in EleventyExtensionMap."); } return this.#engineManager; } set engineManager(mgr) { this.#engineManager = mgr; } reset() { this.#engineManager.reset(); } /* Used for layout path resolution */ getFileList(path, dir) { if (!path) { return []; } let files = []; this.validTemplateLanguageKeys.forEach((key) => { this.getExtensionsFromKey(key).forEach(function (extension) { files.push((dir ? dir + "/" : "") + path + "." + extension); }); }); return files; } // Warning: this would false positive on an include, but is only used // on paths found from the file system glob search. // TODO: Method name might just need to be renamed to something more accurate. isFullTemplateFilePath(path) { for (let extension of this.validTemplateLanguageKeys) { if (path.endsWith(`.${extension}`)) { return true; } } return false; } getCustomExtensionEntry(extension) { if (!this.config.extensionMap) { return; } for (let entry of this.config.extensionMap) { if (entry.extension === extension) { return entry; } } } getValidExtensionsForPath(path) { let extensions = new Set(); for (let extension in this.extensionToKeyMap) { if (path.endsWith(`.${extension}`)) { extensions.add(extension); } } // if multiple extensions are valid, sort from longest to shortest // e.g. .11ty.js and .js let sorted = Array.from(extensions) .filter((extension) => this.validTemplateLanguageKeys.includes(extension)) .sort((a, b) => b.length - a.length); return sorted; } async shouldSpiderJavaScriptDependencies(path) { let extensions = this.getValidExtensionsForPath(path); for (let extension of extensions) { if (extension in this._spiderJsDepsCache) { return this._spiderJsDepsCache[extension]; } let cls = await this.engineManager.getEngineClassByExtension(extension); if (cls) { let entry = this.getCustomExtensionEntry(extension); let shouldSpider = cls.shouldSpiderJavaScriptDependencies(entry); this._spiderJsDepsCache[extension] = shouldSpider; return shouldSpider; } } return false; } getPassthroughCopyGlobs(inputDir) { return this._getGlobs(this.passthroughCopyKeys, inputDir); } getValidGlobs(inputDir) { return this._getGlobs(this.validTemplateLanguageKeys, inputDir); } getGlobs(inputDir) { return this._getGlobs(this.unfilteredFormatKeys, inputDir); } _getGlobs(formatKeys, inputDir = "") { let extensions = new Set(); for (let key of formatKeys) { if (this.hasExtension(key)) { for (let extension of this.getExtensionsFromKey(key)) { extensions.add(extension); } } else { extensions.add(key); } } let dir = TemplatePath.convertToRecursiveGlobSync(inputDir); if (extensions.size === 1) { return [`${dir}/*.${Array.from(extensions)[0]}`]; } else if (extensions.size > 1) { return [ // extra curly brackets /*.{cjs,txt} `${dir}/*.{${Array.from(extensions).join(",")}}`, ]; } return []; } hasExtension(key) { for (let extension in this.extensionToKeyMap) { if ( this.extensionToKeyMap[extension].key === key || this.extensionToKeyMap[extension].aliasKey === key ) { return true; } } return false; } getExtensionsFromKey(key) { let extensions = new Set(); for (let extension in this.extensionToKeyMap) { if (this.extensionToKeyMap[extension].aliasKey) { // only add aliased extension if explicitly referenced in formats // overrides will not have an aliasKey (md => md) if (this.extensionToKeyMap[extension].aliasKey === key) { extensions.add(extension); } } else if (this.extensionToKeyMap[extension].key === key) { extensions.add(extension); } } return Array.from(extensions); } // Only `addExtension` configuration API extensions getExtensionEntriesFromKey(key) { let entries = new Set(); if ("extensionMap" in this.config) { for (let entry of this.config.extensionMap) { if (entry.key === key) { entries.add(entry); } } } return Array.from(entries); } // Determines whether a path is a passthrough copy file or a template (via TemplateWriter) hasEngine(pathOrKey) { return !!this.getKey(pathOrKey); } getKey(pathOrKey) { pathOrKey = (pathOrKey || "").toLowerCase(); for (let extension in this.extensionToKeyMap) { if (pathOrKey === extension || pathOrKey.endsWith("." + extension)) { let key = this.extensionToKeyMap[extension].aliasKey || this.extensionToKeyMap[extension].key; // must be a valid format key passed (e.g. via --formats) if (this.validTemplateLanguageKeys.includes(key)) { return key; } } } } getExtensionEntry(pathOrKey) { pathOrKey = (pathOrKey || "").toLowerCase(); for (let extension in this.extensionToKeyMap) { if (pathOrKey === extension || pathOrKey.endsWith("." + extension)) { return this.extensionToKeyMap[extension]; } } } removeTemplateExtension(path) { for (let extension in this.extensionToKeyMap) { if (path === extension || path.endsWith("." + extension)) { return path.slice( 0, path.length - 1 - extension.length < 0 ? 0 : path.length - 1 - extension.length, ); } } return path; } // keys are file extensions // values are template language keys get extensionToKeyMap() { if (!this._extensionToKeyMap) { this._extensionToKeyMap = { md: { key: "md", extension: "md" }, html: { key: "html", extension: "html" }, njk: { key: "njk", extension: "njk" }, liquid: { key: "liquid", extension: "liquid" }, "11ty.js": { key: "11ty.js", extension: "11ty.js" }, "11ty.cjs": { key: "11ty.js", extension: "11ty.cjs" }, "11ty.mjs": { key: "11ty.js", extension: "11ty.mjs" }, }; if (isTypeScriptSupported()) { this._extensionToKeyMap["11ty.ts"] = { key: "11ty.js", extension: "11ty.ts" }; this._extensionToKeyMap["11ty.cts"] = { key: "11ty.js", extension: "11ty.cts" }; this._extensionToKeyMap["11ty.mts"] = { key: "11ty.js", extension: "11ty.mts" }; } if ("extensionMap" in this.config) { for (let entry of this.config.extensionMap) { // extension and key are only different when aliasing. this._extensionToKeyMap[entry.extension] = entry; } } } return this._extensionToKeyMap; } getReadableFileExtensions() { return Object.keys(this.extensionToKeyMap).join(" "); } } export default EleventyExtensionMap; ================================================ FILE: src/EleventyFiles.js ================================================ import { existsSync, statSync, readFileSync } from "node:fs"; import { TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import DirContains from "./Util/DirContains.js"; import TemplateData from "./Data/TemplateData.js"; import TemplateGlob from "./TemplateGlob.js"; import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js"; const debug = debugUtil("Eleventy:EleventyFiles"); class EleventyFiles { #extensionMap; #watcherGlobs; constructor(formats, templateConfig) { if (!templateConfig) { throw new Error("Internal error: Missing `templateConfig`` argument."); } this.templateConfig = templateConfig; this.config = templateConfig.getConfig(); this.aggregateBench = this.config.benchmarkManager.get("Aggregate"); this.formats = formats; this.eleventyIgnoreContent = false; } get dirs() { return this.templateConfig.directories; } get inputDir() { return this.dirs.input; } get outputDir() { return this.dirs.output; } get includesDir() { return this.dirs.includes; } get layoutsDir() { return this.dirs.layouts; } get dataDir() { return this.dirs.data; } // Backwards compat getDataDir() { return this.dataDir; } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } init() { if (this.dirs.inputFile || this.dirs.inputGlob) { this.templateGlobs = TemplateGlob.map([this.dirs.inputFile || this.dirs.inputGlob]); } else { // Input is a directory this.templateGlobs = this.extensionMap.getGlobs(this.inputDir); } this.setupGlobs(); } #getWatcherGlobs() { if (!this.#watcherGlobs) { let globs; // Input is a file if (this.inputFile) { globs = this.templateGlobs; } else { // input is a directory globs = this.extensionMap.getValidGlobs(this.inputDir); } this.#watcherGlobs = globs; } return this.#watcherGlobs; } get passthroughGlobs() { let paths = new Set(); // stuff added in addPassthroughCopy() for (let path of this.passthroughManager.getConfigPathGlobs()) { paths.add(path); } // non-template language extensions for (let path of this.extensionMap.getPassthroughCopyGlobs(this.inputDir)) { paths.add(path); } return Array.from(paths); } restart() { this.setupGlobs(); this._glob = null; } /* For testing */ _setConfig(config) { if (!config.ignores) { config.ignores = new Set(); config.ignores.add("**/node_modules/**"); } this.config = config; this.init(); } /* Set command root for local project paths */ // This is only used by tests _setLocalPathRoot(dir) { this.localPathRoot = dir; } set extensionMap(extensionMap) { this.#extensionMap = extensionMap; } get extensionMap() { // for tests if (!this.#extensionMap) { throw new Error("Internal error: missing `extensionMap` in EleventyFiles."); } return this.#extensionMap; } setRunMode(runMode) { this.runMode = runMode; } setPassthroughManager(mgr) { this.passthroughManager = mgr; } set templateData(templateData) { this._templateData = templateData; } get templateData() { return this._templateData; } setupGlobs() { this.fileIgnores = this.getIgnores(); this.extraIgnores = this.getIncludesAndDataDirs(); this.uniqueIgnores = this.getIgnoreGlobs(); // Conditional added for tests that don’t have a config if (this.config?.events) { this.config.events.emit("eleventy.ignores", this.uniqueIgnores); } this.normalizedTemplateGlobs = this.templateGlobs; } normalizeIgnoreEntry(entry) { if (!entry.startsWith("**/")) { return TemplateGlob.normalizePath(this.localPathRoot || ".", entry); } return entry; } getIgnoreGlobs() { let uniqueIgnores = new Set(); for (let ignore of this.fileIgnores) { uniqueIgnores.add(ignore); } for (let ignore of this.extraIgnores) { uniqueIgnores.add(ignore); } // Placing the config ignores last here is important to the tests for (let ignore of this.config.ignores) { uniqueIgnores.add(this.normalizeIgnoreEntry(ignore)); } return Array.from(uniqueIgnores); } static getFileIgnores(ignoreFiles) { if (!Array.isArray(ignoreFiles)) { ignoreFiles = [ignoreFiles]; } let ignores = []; for (let ignorePath of ignoreFiles) { ignorePath = TemplatePath.normalize(ignorePath); let dir = TemplatePath.getDirFromFilePath(ignorePath); if (existsSync(ignorePath)) { let ignoreContent = readFileSync(ignorePath, "utf8"); if (ignoreContent) { ignores = ignores.concat(EleventyFiles.normalizeIgnoreContent(dir, ignoreContent)); } } } ignores.forEach((path) => debug(`${ignoreFiles} ignoring: ${path}`)); return ignores; } static normalizeIgnoreContent(dir, ignoreContent) { let ignores = []; if (ignoreContent) { ignores = ignoreContent .split("\n") .map((line) => { return line.trim(); }) .filter((line) => { if (line.charAt(0) === "!") { debug( ">>> When processing .gitignore/.eleventyignore, Eleventy does not currently support negative patterns but encountered one:", ); debug(">>>", line); debug("Follow along at https://github.com/11ty/eleventy/issues/693 to track support."); } // empty lines or comments get filtered out return line.length > 0 && line.charAt(0) !== "#" && line.charAt(0) !== "!"; }) .map((line) => { let path = TemplateGlob.normalizePath(dir, "/", line); path = TemplatePath.addLeadingDotSlash(TemplatePath.relativePath(path)); try { // Note these folders must exist to get /** suffix let stat = statSync(path); if (stat.isDirectory()) { return path + "/**"; } return path; } catch (e) { return path; } }); } return ignores; } /* Tests only */ _setEleventyIgnoreContent(content) { this.eleventyIgnoreContent = content; } getIgnores() { let files = new Set(); for (let ignore of EleventyFiles.getFileIgnores(this.getIgnoreFiles())) { files.add(ignore); } // testing API if (this.eleventyIgnoreContent !== false) { files.add(this.eleventyIgnoreContent); } // Make sure output dir isn’t in the input dir (or it will ignore all input!) // input: . and output: . (skip ignore) // input: ./content and output . (skip ignore) // input: . and output: ./_site (add ignore) let outputContainsInputDir = DirContains(this.outputDir, this.inputDir); if (!outputContainsInputDir) { // both are already normalized in 3.0 files.add(TemplateGlob.map(this.outputDir + "/**")); } return Array.from(files); } getIgnoreFiles() { let ignoreFiles = new Set(); let rootDirectory = this.localPathRoot || "."; if (this.config.useGitIgnore) { ignoreFiles.add(TemplatePath.join(rootDirectory, ".gitignore")); } if (this.eleventyIgnoreContent === false) { let absoluteInputDir = TemplatePath.absolutePath(this.inputDir); ignoreFiles.add(TemplatePath.join(rootDirectory, ".eleventyignore")); if (rootDirectory !== absoluteInputDir) { ignoreFiles.add(TemplatePath.join(this.inputDir, ".eleventyignore")); } } return Array.from(ignoreFiles); } /* Backwards compat */ getIncludesDir() { return this.includesDir; } /* Backwards compat */ getLayoutsDir() { return this.layoutsDir; } getFileGlobs() { return this.normalizedTemplateGlobs; } getRawFiles() { return this.templateGlobs; } async getWatchPathCache() { // Issue #1325: make sure passthrough copy files are not included here if (!this.pathCache) { throw new Error("Watching requires `.getFiles()` to be called first in EleventyFiles"); } let ret = []; // Filter out the passthrough copy paths. for (let path of this.pathCache) { if ( this.extensionMap.isFullTemplateFilePath(path) && (await this.extensionMap.shouldSpiderJavaScriptDependencies(path)) ) { ret.push(path); } } return ret; } _globSearch() { let globs = this.getFileGlobs(); // returns a promise debug("Searching for: %o", globs); return this.fileSystemSearch.search("templates", globs, { ignore: this.uniqueIgnores, }); } async getFiles() { let bench = this.aggregateBench.get("Searching the file system (templates)"); bench.before(); let globResults = await this._globSearch(); let paths = TemplatePath.addLeadingDotSlashArray(globResults); bench.after(); // Note 2.0.0-canary.19 removed a `filter` option for custom template syntax here that was unpublished and unused. this.pathCache = paths; return paths; } getFileShape(paths, filePath) { if (!filePath) { return; } if (this.isPassthroughCopyFile(paths, filePath)) { return "copy"; } if (this.isFullTemplateFile(paths, filePath)) { return "template"; } // include/layout/unknown } isPassthroughCopyFile(paths, filePath) { return this.passthroughManager.isPassthroughCopyFile(paths, filePath); } // Assumption here that filePath is not a passthrough copy file isFullTemplateFile(paths, filePath) { if (!filePath) { return false; } for (let path of paths) { if (path === filePath) { return true; } } return false; } /* For `eleventy --watch` */ getGlobWatcherFiles() { let directoryGlobs = this.getIncludesAndDataDirs(); let globs = this.#getWatcherGlobs(); if (checkPassthroughCopyBehavior(this.config, this.runMode)) { return globs.concat(directoryGlobs); } // Revert to old passthroughcopy copy files behavior return globs.concat(this.passthroughGlobs).concat(directoryGlobs); } /* For `eleventy --watch` */ getGlobWatcherFilesForPassthroughCopy() { return this.passthroughGlobs; } /* For `eleventy --watch` */ async getGlobWatcherTemplateDataFiles() { let templateData = this.templateData; return await templateData.getTemplateDataFileGlob(); } /* For `eleventy --watch` */ // TODO this isn’t great but reduces complexity avoiding using TemplateData:getLocalDataPaths for each template in the cache async getWatcherTemplateJavaScriptDataFiles() { let globs = this.templateData.getTemplateJavaScriptDataFileGlob(); let bench = this.aggregateBench.get("Searching the file system (watching)"); bench.before(); let results = TemplatePath.addLeadingDotSlashArray( await this.fileSystemSearch.search("js-dependencies", globs, { ignore: [ "**/node_modules/**", ".git/**", // TODO outputDir // this.outputDir, ], }), ); bench.after(); return results; } /* Ignored by `eleventy --watch` */ getGlobWatcherIgnores() { // convert to format without ! since they are passed in as a separate argument to glob watcher let entries = new Set( this.fileIgnores.map((ignore) => TemplatePath.stripLeadingDotSlash(ignore)), ); for (let ignore of this.config.watchIgnores) { entries.add(this.normalizeIgnoreEntry(ignore)); } // de-duplicated return Array.from(entries); } getIncludesAndDataDirs() { let rawPaths = new Set(); rawPaths.add(this.includesDir); if (this.layoutsDir) { rawPaths.add(this.layoutsDir); } rawPaths.add(this.dataDir); return Array.from(rawPaths) .filter((entry) => { // never ignore the input directory (even if config file returns "" for these) return entry && entry !== this.inputDir; }) .map((entry) => { return TemplateGlob.map(entry + "**"); }); } } export default EleventyFiles; ================================================ FILE: src/EleventyServe.js ================================================ import debugUtil from "debug"; import { Merge, TemplatePath } from "@11ty/eleventy-utils"; import { Watch } from "./Watch.js"; function stringifyOptions(options) { return JSON.stringify(options, function replacer(key, value) { if (typeof value === "function") { return value.toString(); } return value; }); } import EleventyBaseError from "./Errors/EleventyBaseError.js"; import ConsoleLogger from "./Util/ConsoleLogger.js"; import PathPrefixer from "./Util/PathPrefixer.js"; import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js"; import { getModulePackageJson } from "./Util/ImportJsonSync.js"; import { EleventyImport } from "./Util/Require.js"; import { isGlobMatch } from "./Util/GlobMatcher.js"; const debug = debugUtil("Eleventy:EleventyServe"); class EleventyServeConfigError extends EleventyBaseError {} const DEFAULT_SERVER_OPTIONS = { module: "@11ty/eleventy-dev-server", port: 8080, // pathPrefix: "/", // setup: function() {}, // ready: function(server) {}, // logger: { info: function() {}, error: function() {} } }; class EleventyServe { #eleventyConfig; #savedConfigOptions; #aliases; #initOptionsFetched = false; #chokidar; // these are *not* normalized #watchTargets = new Set(); constructor() { this.logger = new ConsoleLogger(); } get config() { if (!this.eleventyConfig) { throw new EleventyServeConfigError( "You need to set the eleventyConfig property on EleventyServe.", ); } return this.eleventyConfig.getConfig(); } set config(config) { throw new Error("It’s not allowed to set config on EleventyServe. Set eleventyConfig instead."); } setAliases(aliases) { this.#aliases = aliases; if (this._server && "setAliases" in this._server) { this._server.setAliases(aliases); } } get eleventyConfig() { if (!this.#eleventyConfig) { throw new EleventyServeConfigError( "You need to set the eleventyConfig property on EleventyServe.", ); } return this.#eleventyConfig; } set eleventyConfig(config) { this.#eleventyConfig = config; if (checkPassthroughCopyBehavior(this.#eleventyConfig.userConfig, "serve")) { this.#eleventyConfig.userConfig.events.on("eleventy.passthrough", ({ map }) => { // for-free passthrough copy this.setAliases(map); }); } } // TODO directorynorm setOutputDir(outputDir) { // TODO check if this is different and if so, restart server (if already running) // This applies if you change the output directory in your config file during watch/serve this.outputDir = outputDir; } static getDevServer() { // This happens on demand for performance purposes when not used by builds // https://github.com/11ty/eleventy/pull/3689 return import("@11ty/eleventy-dev-server").then((i) => i.default); } async getServerModule(name) { try { if (!name || name === DEFAULT_SERVER_OPTIONS.module) { return EleventyServe.getDevServer(); } // Look for peer dep in local project let projectNodeModulesPath = TemplatePath.absolutePath("./node_modules/"); let serverPath = TemplatePath.absolutePath(projectNodeModulesPath, name); // No references outside of the project node_modules are allowed if (!serverPath.startsWith(projectNodeModulesPath)) { throw new Error("Invalid node_modules name for Eleventy server instance, received:" + name); } let serverPackageJson = getModulePackageJson(serverPath); // Normalize with `main` entry from if (TemplatePath.isDirectorySync(serverPath)) { if (serverPackageJson.main) { serverPath = TemplatePath.absolutePath( projectNodeModulesPath, name, serverPackageJson.main, ); } else { throw new Error( `Eleventy server ${name} is missing a \`main\` entry in its package.json file. Traversed up from ${serverPath}.`, ); } } let module = await EleventyImport(serverPath); if (!("getServer" in module)) { throw new Error( `Eleventy server module requires a \`getServer\` static method. Could not find one on module: \`${name}\``, ); } if (serverPackageJson["11ty"]?.compatibility) { try { this.eleventyConfig.userConfig.versionCheck(serverPackageJson["11ty"].compatibility); } catch (e) { this.logger.warn(`Warning: \`${name}\` Plugin Compatibility: ${e.message}`); } } return module; } catch (e) { this.logger.error( "There was an error with your custom Eleventy server. We’re using the default server instead.\n" + e.message, ); debug("Eleventy server error %o", e); return EleventyServe.getDevServer(); } } get options() { if (this._options) { return this._options; } this._options = Object.assign( { pathPrefix: PathPrefixer.normalizePathPrefix(this.config.pathPrefix), logger: this.logger, }, DEFAULT_SERVER_OPTIONS, this.config.serverOptions, ); this.#savedConfigOptions = this.config.serverOptions; if (!this.#initOptionsFetched && this.getSetupCallback()) { throw new Error( "Init options have not yet been fetched in the setup callback. This probably means that `init()` has not yet been called.", ); } return this._options; } get server() { if (!this._server) { throw new Error("Missing server instance. Did you call .initServerInstance?"); } return this._server; } async initServerInstance() { if (this._server) { return; } let serverModule = await this.getServerModule(this.options.module); if (this.options.module === DEFAULT_SERVER_OPTIONS.module) { // Fix for missing globs in Chokidar@4 let copyWatch = new Watch(this.eleventyConfig); copyWatch.watchTargets(this.#watchTargets); // Careful with ignores here: https://github.com/11ty/eleventy/issues/1134 // copyWatch.addIgnores(); await copyWatch.start(); this.#chokidar = copyWatch; this.options.chokidar = copyWatch; } // Static method `getServer` was already checked in `getServerModule` this._server = serverModule.getServer("eleventy-server", this.outputDir, this.options); this.setAliases(this.#aliases); } getSetupCallback() { let setupCallback = this.config.serverOptions.setup; if (setupCallback && typeof setupCallback === "function") { return setupCallback; } } async #init() { let setupCallback = this.getSetupCallback(); if (setupCallback) { let opts = await setupCallback(); this.#initOptionsFetched = true; if (opts) { Merge(this.options, opts); } } } async init() { if (!this._initPromise) { this._initPromise = this.#init(); } return this._initPromise; } // Port comes in here from --port on the command line async serve(port) { this._commandLinePort = port; await this.init(); await this.initServerInstance(); this.server.serve(port || this.options.port); if (typeof this.config.serverOptions?.ready === "function") { if (typeof this.server.ready === "function") { // Dev Server 2.0.7+ // wait for ready promise to resolve before triggering ready callback await this.server.ready(); await this.config.serverOptions?.ready(this.server); } else { throw new Error( "The `ready` option in Eleventy’s `setServerOptions` method requires a `ready` function on the Dev Server instance. If you’re using Eleventy Dev Server, you will need Dev Server 2.0.7+ or newer to use this feature.", ); } } } async close() { if (this._server) { await this._server.close(); this._server = undefined; } } async sendError({ error }) { if (this._server) { await this.server.sendError({ error, }); } } // when the configuration file changes (but server options *may* not, which would otherwise trigger restart()) resetConfig() { this.#watchTargets = new Set(); } // Restart the server entirely // We don’t want to use a native `restart` method (e.g. restart() in Vite) so that // we can correctly handle a `module` property change (changing the server type) async restart() { // Blow away cached options delete this._options; await this.close(); // saved --port in `serve()` await this.serve(this._commandLinePort); } // checkPassthroughCopyBehavior check is called upstream in Eleventy.js watchPassthroughCopy(globs = []) { for (let glob of globs) { this.#watchTargets.add(glob); } // if the watcher has already started, add the targets if (this.#chokidar) { this.#chokidar.watchTargets(globs); } } isEmulatedPassthroughCopyMatch(filepath) { return isGlobMatch(filepath, Array.from(this.#watchTargets)); } hasOptionsChanged() { return ( stringifyOptions(this.config.serverOptions) !== stringifyOptions(this.#savedConfigOptions) ); } // Live reload the server async reload(reloadEvent = {}) { if (!this._server) { return; } // Restart the server if the options have changed if (this.hasOptionsChanged()) { debug("Server options changed, we’re restarting the server"); await this.restart(); } else { await this.server.reload(reloadEvent); } } } export default EleventyServe; ================================================ FILE: src/Engines/Custom.js ================================================ import TemplateEngine from "./TemplateEngine.js"; import getJavaScriptData from "../Util/GetJavaScriptData.js"; export default class CustomEngine extends TemplateEngine { constructor(name, eleventyConfig) { super(name, eleventyConfig); this.entry = this.getExtensionMapEntry(); this.needsInit = "init" in this.entry && typeof this.entry.init === "function"; this.setDefaultEngine(undefined); } getExtensionMapEntry() { if ("extensionMap" in this.config) { let name = this.name.toLowerCase(); // Iterates over only the user config `addExtension` entries for (let entry of this.config.extensionMap) { let entryKey = (entry.aliasKey || entry.key || "").toLowerCase(); if (entryKey === name) { return entry; } } } throw Error( `Could not find a custom extension for ${this.name}. Did you add it to your config file?`, ); } setDefaultEngine(defaultEngine) { this._defaultEngine = defaultEngine; } get cacheable() { // Enable cacheability for this template if (this.entry?.compileOptions?.cache !== undefined) { return this.entry.compileOptions.cache; } else if (this.needsToReadFileContents()) { return true; } else if (this._defaultEngine?.cacheable !== undefined) { return this._defaultEngine.cacheable; } return super.cacheable; } async getInstanceFromInputPath(inputPath) { if ( "getInstanceFromInputPath" in this.entry && typeof this.entry.getInstanceFromInputPath === "function" ) { // returns Promise return this.entry.getInstanceFromInputPath(inputPath); } // aliased upstream type if ( this._defaultEngine && "getInstanceFromInputPath" in this._defaultEngine && typeof this._defaultEngine.getInstanceFromInputPath === "function" ) { // returns Promise return this._defaultEngine.getInstanceFromInputPath(inputPath); } return false; } /** * Whether to use the module loader directly * * @override */ useJavaScriptImport() { if ("useJavaScriptImport" in this.entry) { return this.entry.useJavaScriptImport; } if ( this._defaultEngine && "useJavaScriptImport" in this._defaultEngine && typeof this._defaultEngine.useJavaScriptImport === "function" ) { return this._defaultEngine.useJavaScriptImport(); } return false; } /** * @override */ needsToReadFileContents() { if ("read" in this.entry) { return this.entry.read; } // Handle aliases to `11ty.js` templates, avoid reading files in the alias, see #2279 // Here, we are short circuiting fallback to defaultRenderer, does not account for compile // functions that call defaultRenderer explicitly if (this._defaultEngine && "needsToReadFileContents" in this._defaultEngine) { return this._defaultEngine.needsToReadFileContents(); } return true; } // If we init from multiple places, wait for the first init to finish before continuing on. async _runningInit() { if (this.needsInit) { if (!this._initing) { this._initBench = this.benchmarks.aggregate.get(`Engine (${this.name}) Init`); this._initBench.before(); this._initing = this.entry.init.bind({ config: this.config, bench: this.benchmarks.aggregate, })(); } await this._initing; this.needsInit = false; if (this._initBench) { this._initBench.after(); this._initBench = undefined; } } } async getExtraDataFromFile(inputPath) { if (this.entry.getData === false) { return; } if (!("getData" in this.entry)) { // Handle aliases to `11ty.js` templates, use upstream default engine data fetch, see #2279 if (this._defaultEngine && "getExtraDataFromFile" in this._defaultEngine) { return this._defaultEngine.getExtraDataFromFile(inputPath); } return; } await this._runningInit(); if (typeof this.entry.getData === "function") { let dataBench = this.benchmarks.aggregate.get( `Engine (${this.name}) Get Data From File (Function)`, ); dataBench.before(); let data = this.entry.getData(inputPath); dataBench.after(); return data; } let keys = new Set(); if (this.entry.getData === true) { keys.add("data"); } else if (Array.isArray(this.entry.getData)) { for (let key of this.entry.getData) { keys.add(key); } } let dataBench = this.benchmarks.aggregate.get(`Engine (${this.name}) Get Data From File`); dataBench.before(); let inst = await this.getInstanceFromInputPath(inputPath); if (inst === false) { dataBench.after(); return Promise.reject( new Error( `\`getInstanceFromInputPath\` callback missing from '${this.name}' template engine plugin. It is required when \`getData\` is in use. You can set \`getData: false\` to opt-out of this.`, ), ); } // override keys set at the plugin level in the individual template if (inst.eleventyDataKey) { keys = new Set(inst.eleventyDataKey); } let mixins; if (this.config) { // Object.assign usage: see TemplateRenderCustomTest.js: `JavaScript functions should not be mutable but not *that* mutable` mixins = Object.assign({}, this.config.javascriptFunctions); } let promises = []; for (let key of keys) { promises.push( getJavaScriptData(inst, inputPath, key, { mixins, isObjectRequired: key === "data", }), ); } let results = await Promise.all(promises); let data = {}; for (let result of results) { Object.assign(data, result); } dataBench.after(); return data; } async compile(str, inputPath, ...args) { await this._runningInit(); let defaultCompilationFn; if (this._defaultEngine) { defaultCompilationFn = async (data) => { const renderFn = await this._defaultEngine.compile(str, inputPath, ...args); return renderFn(data); }; } // Fall back to default compiler if the user does not provide their own if (!this.entry.compile) { if (defaultCompilationFn) { return defaultCompilationFn; } else { throw new Error( `Missing \`compile\` property for custom template syntax definition eleventyConfig.addExtension("${this.name}"). This is not necessary when aliasing to an existing template syntax.`, ); } } // TODO generalize this (look at JavaScript.js) let compiledFn = this.entry.compile.bind({ config: this.config, addDependencies: (from, toArray = []) => { this.config.uses.addDependency(from, toArray); }, defaultRenderer: defaultCompilationFn, // bind defaultRenderer to compile function })(str, inputPath); // Support `undefined` to skip compile/render if (compiledFn) { // Bind defaultRenderer to render function if ("then" in compiledFn && typeof compiledFn.then === "function") { // Promise, wait to bind return compiledFn.then((fn) => { if (typeof fn === "function") { return fn.bind({ defaultRenderer: defaultCompilationFn, }); } return fn; }); } else if ("bind" in compiledFn && typeof compiledFn.bind === "function") { return compiledFn.bind({ defaultRenderer: defaultCompilationFn, }); } } return compiledFn; } get defaultTemplateFileExtension() { return this.entry.outputFileExtension ?? "html"; } // Whether or not to wrap in Eleventy layouts useLayouts() { // TODO future change fallback to `this.defaultTemplateFileExtension === "html"` return this.entry.useLayouts ?? true; } hasDependencies(inputPath) { if (this.config.uses.getDependencies(inputPath) === false) { return false; } return true; } isFileRelevantTo(inputPath, comparisonFile, includeLayouts) { return this.config.uses.isFileRelevantTo(inputPath, comparisonFile, includeLayouts); } getCompileCacheKey(str, inputPath) { let lastModifiedFile = this.eleventyConfig.getPreviousBuildModifiedFile(); // Return this separately so we know whether or not to use the cached version // but still return a key to cache this new render for next time let isRelevant = this.isFileRelevantTo(inputPath, lastModifiedFile, false); let useCache = !isRelevant; if (this.entry.compileOptions && "getCacheKey" in this.entry.compileOptions) { if (typeof this.entry.compileOptions.getCacheKey !== "function") { throw new Error( `\`compileOptions.getCacheKey\` must be a function in addExtension for the ${this.name} type`, ); } return { useCache, key: this.entry.compileOptions.getCacheKey(str, inputPath), }; } let { key } = super.getCompileCacheKey(str, inputPath); return { useCache, key, }; } permalinkNeedsCompilation(/*str*/) { if (this.entry.compileOptions && "permalink" in this.entry.compileOptions) { let p = this.entry.compileOptions.permalink; if (p === "raw") { return false; } // permalink: false is aliased to permalink: () => false if (p === false) { return () => false; } return this.entry.compileOptions.permalink; } // Breaking: default changed from `true` to `false` in 3.0.0-alpha.13 // Note: `false` is the same as "raw" here. return false; } static shouldSpiderJavaScriptDependencies(entry) { if (entry.compileOptions && "spiderJavaScriptDependencies" in entry.compileOptions) { return entry.compileOptions.spiderJavaScriptDependencies; } return false; } } ================================================ FILE: src/Engines/FrontMatter/JavaScript.js ================================================ import { RetrieveGlobals } from "../../Util/RetrieveGlobals.js"; // `javascript` Front Matter Type export default function (frontMatterCode, context = {}) { let { filePath } = context; // Legacy `javascript` type was removed in @11ty/gray-matter@2 to avoid eval() let trimmed = frontMatterCode.trimStart(); if (trimmed.startsWith("{")) { return RetrieveGlobals(`export default ${trimmed}`, filePath, { isJavaScriptFrontMatterCompat: true, // turns off implicit exports }).then((res) => { return res.default; }); } return RetrieveGlobals(frontMatterCode, filePath); } ================================================ FILE: src/Engines/Html.js ================================================ import TemplateEngine from "./TemplateEngine.js"; export default class Html extends TemplateEngine { constructor(name, eleventyConfig) { super(name, eleventyConfig); } get cacheable() { return true; } async #getPreEngine(preTemplateEngine) { return this.engineManager.getEngine(preTemplateEngine, this.extensionMap); } async compile(str, inputPath, preTemplateEngine) { if (preTemplateEngine) { let engine = await this.#getPreEngine(preTemplateEngine); let fnReady = engine.compile(str, inputPath); return async function (data) { let fn = await fnReady; return fn(data); }; } return function () { // do nothing with data if preTemplateEngine is falsy return str; }; } } ================================================ FILE: src/Engines/JavaScript.js ================================================ import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import TemplateEngine from "./TemplateEngine.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import getJavaScriptData from "../Util/GetJavaScriptData.js"; import { EleventyImport } from "../Util/Require.js"; import { augmentFunction, augmentObject } from "./Util/ContextAugmenter.js"; class JavaScriptTemplateNotDefined extends EleventyBaseError {} export default class JavaScript extends TemplateEngine { constructor(name, templateConfig) { super(name, templateConfig); this.instances = {}; this.config.events.on("eleventy#templateModified", (inputPath, metadata = {}) => { let { usedByDependants, relevantLayouts } = metadata; // Remove from cached instances when modified let instancesToDelete = [ inputPath, ...(usedByDependants || []), ...(relevantLayouts || []), ].map((entry) => TemplatePath.addLeadingDotSlash(entry)); for (let inputPath of instancesToDelete) { if (inputPath in this.instances) { delete this.instances[inputPath]; } } }); } get cacheable() { return false; } normalize(result) { if (Buffer.isBuffer(result)) { return result.toString(); } return result; } // String, Buffer, Promise // Function, Class // Object // Module _getInstance(mod) { let noop = function () { return ""; }; let originalModData = mod?.data; if (typeof mod === "object" && mod.default && this.eleventyConfig.getIsProjectUsingEsm()) { mod = mod.default; } if (typeof mod === "string" || mod instanceof Buffer || mod.then) { return { render: () => mod }; } else if (typeof mod === "function") { if (mod.prototype?.data || mod.prototype?.render) { if (!("render" in mod.prototype)) { mod.prototype.render = noop; } if (!("data" in mod.prototype) && !mod.data && originalModData) { mod.prototype.data = originalModData; } return new mod(); } else { // JavaScript lol if (mod.toString().trimStart().startsWith("class ")) { // we already know that mod.prototype.data and mod.prototype.render do not exist (per above) // now we look for instance properties for `data` or `render`, issue #1645 let modInstance = new mod(); if (Object.hasOwn(modInstance, "data") || Object.hasOwn(modInstance, "render")) { if (!Object.hasOwn(modInstance, "render")) { mod.prototype.render = noop; } if (!Object.hasOwn(modInstance, "data") && !mod.data && originalModData) { mod.prototype.data = originalModData; } return modInstance; } else { throw new Error( "Invalid class signature for an 11ty.js template: needs a render or data instance property.", ); } } return { ...(originalModData ? { data: originalModData } : undefined), render: mod, }; } } else if ("data" in mod || "render" in mod) { if (!mod.render) { mod.render = noop; } if (!mod.data && originalModData) { mod.data = originalModData; } return mod; } } async #getInstanceFromInputPath(inputPath) { let mod; let relativeInputPath = this.eleventyConfig.directories.getInputPathRelativeToInputDirectory(inputPath); if (this.eleventyConfig.userConfig.isVirtualTemplate(relativeInputPath)) { mod = this.eleventyConfig.userConfig.virtualTemplates[relativeInputPath].content; } else { let isEsm = this.eleventyConfig.getIsProjectUsingEsm(); let cacheBust = !this.cacheable || !this.config.useTemplateCache; mod = await EleventyImport(inputPath, isEsm ? "esm" : "cjs", { cacheBust, }); } let inst = this._getInstance(mod); if (inst) { this.instances[inputPath] = inst; } else { throw new JavaScriptTemplateNotDefined( `No JavaScript template returned from ${inputPath}. Did you assign module.exports (CommonJS) or export (ESM)?`, ); } return inst; } async getInstanceFromInputPath(inputPath) { if (!this.instances[inputPath]) { this.instances[inputPath] = this.#getInstanceFromInputPath(inputPath); } return this.instances[inputPath]; } /** * JavaScript files defer to the module loader rather than read the files to strings * * @override */ needsToReadFileContents() { return false; } /** * Use the module loader directly * * @override */ useJavaScriptImport() { return true; } async getExtraDataFromFile(inputPath) { let inst = await this.getInstanceFromInputPath(inputPath); return getJavaScriptData(inst, inputPath); } getJavaScriptFunctions(inst) { let fns = {}; let configFns = this.config.javascriptFunctions; for (let key in configFns) { // prefer pre-existing `page` javascriptFunction, if one exists fns[key] = augmentFunction(configFns[key], { source: inst, overwrite: false, }); } return fns; } // Backwards compat static wrapJavaScriptFunction(inst, fn) { return augmentFunction(fn, { source: inst, }); } addExportsToBundles(inst, url) { let cfg = this.eleventyConfig.userConfig; if (!("getBundleManagers" in cfg)) { return; } let managers = cfg.getBundleManagers(); for (let name in managers) { let mgr = managers[name]; let key = mgr.getBundleExportKey(); if (!key) { continue; } if (typeof inst[key] === "string") { // export const css = ``; mgr.addToPage(url, inst[key]); } else if (isPlainObject(inst[key])) { if (typeof inst[key][name] === "string") { // Object with bundle names: // export const bundle = { // css: `` // }; mgr.addToPage(url, inst[key][name]); } else if (isPlainObject(inst[key][name])) { // Object with bucket names: // export const bundle = { // css: { // default: `` // } // }; for (let bucketName in inst[key][name]) { mgr.addToPage(url, inst[key][name][bucketName], bucketName); } } } } } async compile(str, inputPath) { let inst; if (str) { // When str has a value, it's being used for permalinks in data inst = this._getInstance(str); } else { // For normal templates, str will be falsy. inst = await this.getInstanceFromInputPath(inputPath); } if (inst?.render) { return (data = {}) => { // TODO does this do anything meaningful for non-classes? // `inst` should have a normalized `render` function from _getInstance // Map exports to bundles if (data.page?.url) { this.addExportsToBundles(inst, data.page.url); } augmentObject(inst, { source: data, overwrite: false, }); Object.assign(inst, this.getJavaScriptFunctions(inst)); return this.normalize(inst.render.call(inst, data)); }; } } static shouldSpiderJavaScriptDependencies() { return true; } } ================================================ FILE: src/Engines/Liquid.js ================================================ import moo from "moo"; import { Tokenizer, TokenKind, evalToken, Liquid as LiquidJs } from "liquidjs"; import { TemplatePath } from "@11ty/eleventy-utils"; // import debugUtil from "debug"; import TemplateEngine from "./TemplateEngine.js"; import { augmentObject } from "./Util/ContextAugmenter.js"; // const debug = debugUtil("Eleventy:Liquid"); export default class Liquid extends TemplateEngine { static argumentLexerOptions = { number: /[0-9]+\.*[0-9]*/, doubleQuoteString: /"(?:\\["\\]|[^\n"\\])*"/, singleQuoteString: /'(?:\\['\\]|[^\n'\\])*'/, keyword: /[a-zA-Z0-9.\-_]+/, "ignore:whitespace": /[, \t]+/, // includes comma separator }; constructor(name, eleventyConfig) { super(name, eleventyConfig); this.liquidOptions = this.config.liquidOptions || {}; this.setLibrary(this.config.libraryOverrides.liquid); this.argLexer = moo.compile(Liquid.argumentLexerOptions); } get cacheable() { return true; } setLibrary(override) { // warning, the include syntax supported here does not exactly match what Jekyll uses. this.liquidLib = override || new LiquidJs(this.getLiquidOptions()); this.setEngineLib(this.liquidLib, Boolean(this.config.libraryOverrides.liquid)); this.addFilters(this.config.liquidFilters); // TODO these all go to the same place (addTag), add warnings for overwrites this.addCustomTags(this.config.liquidTags); this.addAllShortcodes(this.config.liquidShortcodes); this.addAllPairedShortcodes(this.config.liquidPairedShortcodes); } getLiquidOptions() { let defaults = { root: [this.dirs.includes, this.dirs.input], // supplemented in compile with inputPath below extname: ".liquid", strictFilters: true, // cache: true, jsTruthy: true, // Breaking in v4 #3507 }; let options = Object.assign(defaults, this.liquidOptions || {}); // debug("Liquid constructor options: %o", options); return options; } static wrapFilter(name, fn) { /** * @this {object} */ return function (...args) { // Set this.eleventy and this.page if (typeof this.context?.get === "function") { augmentObject(this, { source: this.context, getter: (key, context) => context.get([key]), lazy: this.context.strictVariables, }); } // We *don’t* wrap this in an EleventyFilterError because Liquid has a better error message with line/column information in the template return fn.call(this, ...args); }; } // Shortcodes static normalizeScope(context) { let obj = {}; if (context) { obj.ctx = context; // Full context available on `ctx` // Set this.eleventy and this.page augmentObject(obj, { source: context, getter: (key, context) => context.get([key]), lazy: context.strictVariables, }); } return obj; } addCustomTags(tags) { for (let name in tags) { this.addTag(name, tags[name]); } } addFilters(filters) { for (let name in filters) { this.addFilter(name, filters[name]); } } addFilter(name, filter) { this.liquidLib.registerFilter(name, Liquid.wrapFilter(name, filter)); } addTag(name, tagFn) { let tagObj; if (typeof tagFn === "function") { tagObj = tagFn(this.liquidLib, { evalToken }); } else { throw new Error( "Liquid.addTag expects a callback function to be passed in: addTag(name, function(liquidEngine) { return { parse: …, render: … } })", ); } this.liquidLib.registerTag(name, tagObj); } addAllShortcodes(shortcodes) { for (let name in shortcodes) { this.addShortcode(name, shortcodes[name]); } } addAllPairedShortcodes(shortcodes) { for (let name in shortcodes) { this.addPairedShortcode(name, shortcodes[name]); } } static parseArguments(lexer, str) { let argArray = []; if (!lexer) { lexer = moo.compile(Liquid.argumentLexerOptions); } if (typeof str === "string") { lexer.reset(str); let arg = lexer.next(); while (arg) { /*{ type: 'doubleQuoteString', value: '"test 2"', text: '"test 2"', toString: [Function: tokenToString], offset: 0, lineBreaks: 0, line: 1, col: 1 }*/ if (arg.type.indexOf("ignore:") === -1) { // Push the promise into an array instead of awaiting it here. // This forces the promises to run in order with the correct scope value for each arg. // Otherwise they run out of order and can lead to undefined values for arguments in layout template shortcodes. // console.log( arg.value, scope, engine ); argArray.push(arg.value); } arg = lexer.next(); } } return argArray; } static parseArgumentsBuiltin(args) { let tokenizer = new Tokenizer(args); let parsedArgs = []; let value = tokenizer.readValue(); while (value) { parsedArgs.push(value); tokenizer.skipBlank(); if (tokenizer.peek() === ",") { tokenizer.advance(); } value = tokenizer.readValue(); } tokenizer.end(); return parsedArgs; } addShortcode(shortcodeName, shortcodeFn) { let _t = this; this.addTag(shortcodeName, function (liquidEngine) { return { parse(tagToken) { this.name = tagToken.name; if (_t.config.liquidParameterParsing === "builtin") { this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args); // note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class } else { this.legacyArgs = tagToken.args; } }, render: function* (ctx) { let argArray = []; if (this.legacyArgs) { let rawArgs = Liquid.parseArguments(_t.argLexer, this.legacyArgs); for (let arg of rawArgs) { let b = yield liquidEngine.evalValue(arg, ctx); argArray.push(b); } } else if (this.orderedArgs) { for (let arg of this.orderedArgs) { let b = yield evalToken(arg, ctx); argArray.push(b); } } let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), ...argArray); return ret; }, }; }); } addPairedShortcode(shortcodeName, shortcodeFn) { let _t = this; this.addTag(shortcodeName, function (liquidEngine) { return { parse(tagToken, remainTokens) { this.name = tagToken.name; if (_t.config.liquidParameterParsing === "builtin") { this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args); // note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class } else { this.legacyArgs = tagToken.args; } this.templates = []; var stream = liquidEngine.parser .parseStream(remainTokens) .on("template", (tpl) => this.templates.push(tpl)) .on("tag:end" + shortcodeName, () => stream.stop()) .on("end", () => { throw new Error(`tag ${tagToken.raw} not closed`); }); stream.start(); }, render: function* (ctx /*, emitter*/) { let argArray = []; if (this.legacyArgs) { let rawArgs = Liquid.parseArguments(_t.argLexer, this.legacyArgs); for (let arg of rawArgs) { let b = yield liquidEngine.evalValue(arg, ctx); argArray.push(b); } } else if (this.orderedArgs) { for (let arg of this.orderedArgs) { let b = yield evalToken(arg, ctx); argArray.push(b); } } const html = yield liquidEngine.renderer.renderTemplates(this.templates, ctx); let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), html, ...argArray); return ret; }, }; }); } parseForSymbols(str) { if (!str) { return []; } let tokenizer = new Tokenizer(str); /** @type {Array} */ let tokens = tokenizer.readTopLevelTokens(); let symbols = tokens .filter((token) => token.kind === TokenKind.Output) .map((token) => { // manually remove filters 😅 return token.content.split("|").map((entry) => entry.trim())[0]; }); return symbols; } // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink) /** @returns {boolean|undefined} */ permalinkNeedsCompilation(str) { if (typeof str === "string") { return this.needsCompilation(str); } } needsCompilation(str) { let options = this.liquidLib.options; return ( str.indexOf(options.tagDelimiterLeft) !== -1 || str.indexOf(options.outputDelimiterLeft) !== -1 ); } async compile(str, inputPath) { let engine = this.liquidLib; let liquidOptions = this.liquidOptions; let tmplReady = engine.parse(str, inputPath); // Required for relative includes let options = {}; if (!inputPath || inputPath === "liquid" || inputPath === "md") { // do nothing } else { options.root = [TemplatePath.getDirFromFilePath(inputPath)]; } return async function (data) { let tmpl = await tmplReady; options.globals = Object.assign( { page: data?.page, eleventy: data?.eleventy, collections: data?.collections, }, liquidOptions?.globals, ); return engine.render(tmpl, data, options); }; } } ================================================ FILE: src/Engines/Markdown.js ================================================ import markdownIt from "markdown-it"; import TemplateEngine from "./TemplateEngine.js"; export default class Markdown extends TemplateEngine { constructor(name, eleventyConfig) { super(name, eleventyConfig); this.markdownOptions = {}; this.setLibrary(this.config.libraryOverrides.md); } get cacheable() { return true; } setLibrary(mdLib) { this.mdLib = mdLib || markdownIt(this.getMarkdownOptions()); // Overrides a highlighter set in `markdownOptions` // This is separate so devs can pass in a new mdLib and still use the official eleventy plugin for markdown highlighting if (this.config.markdownHighlighter && typeof this.mdLib.set === "function") { this.mdLib.set({ highlight: this.config.markdownHighlighter, }); } if (typeof this.mdLib.disable === "function") { // Disable indented code blocks by default (Issue #2438) this.mdLib.disable("code"); } this.setEngineLib(this.mdLib, Boolean(this.config.libraryOverrides.md)); } setMarkdownOptions(options) { this.markdownOptions = options; } getMarkdownOptions() { // work with "mode" presets https://github.com/markdown-it/markdown-it#init-with-presets-and-options if (typeof this.markdownOptions === "string") { return this.markdownOptions; } return Object.assign( { html: true, }, this.markdownOptions || {}, ); } // TODO use preTemplateEngine to help inform this // needsCompilation() { // return super.needsCompilation(); // } async #getPreEngine(preTemplateEngine) { if (typeof preTemplateEngine === "string") { return this.engineManager.getEngine(preTemplateEngine, this.extensionMap); } return preTemplateEngine; } async compile(str, inputPath, preTemplateEngine, bypassMarkdown) { let mdlib = this.mdLib; if (preTemplateEngine) { let engine = await this.#getPreEngine(preTemplateEngine); let fnReady = engine.compile(str, inputPath); if (bypassMarkdown) { return async function (data) { let fn = await fnReady; return fn(data); }; } else { return async function (data) { let fn = await fnReady; let preTemplateEngineRender = await fn(data); let finishedRender = mdlib.render(preTemplateEngineRender, data); return finishedRender; }; } } else { if (bypassMarkdown) { return function () { return str; }; } else { return function (data) { return mdlib.render(str, data); }; } } } } ================================================ FILE: src/Engines/Nunjucks.js ================================================ import debugUtil from "debug"; import { TemplatePath } from "@11ty/eleventy-utils"; // Direct reference to avoid use of `browser` Nunjucks variant in bundles import { default as NunjucksLib, Environment, FileSystemLoader, Template, } from "@11ty/nunjucks/index.js"; import TemplateEngine from "./TemplateEngine.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import { augmentObject } from "./Util/ContextAugmenter.js"; import { withResolvers } from "../Util/PromiseUtil.js"; const debug = debugUtil("Eleventy:Nunjucks"); class EleventyNunjucksError extends EleventyBaseError {} export default class Nunjucks extends TemplateEngine { constructor(name, eleventyConfig) { super(name, eleventyConfig); this.nunjucksEnvironmentOptions = this.config.nunjucksEnvironmentOptions || { dev: true }; this.nunjucksPrecompiledTemplates = this.config.nunjucksPrecompiledTemplates || {}; this._usingPrecompiled = Object.keys(this.nunjucksPrecompiledTemplates).length > 0; this.setLibrary(this.config.libraryOverrides.njk); } // v3.1.0-alpha.1 we’ve moved to use Nunjucks’ internal cache instead of Eleventy’s // get cacheable() { // return false; // } #getFileSystemDirs() { let paths = new Set(); paths.add(super.getIncludesDir()); paths.add(TemplatePath.getWorkingDir()); // Filter out undefined paths return Array.from(paths).filter(Boolean); } #setEnv(override) { if (override) { this.njkEnv = override; } else if (this._usingPrecompiled) { // Precompiled templates to avoid eval! const NodePrecompiledLoader = function () {}; NodePrecompiledLoader.prototype.getSource = (name) => { // https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompiled-loader.js#L5 return { src: { type: "code", obj: this.nunjucksPrecompiledTemplates[name], }, // Maybe add this? // path, // noCache: true }; }; this.njkEnv = new Environment(new NodePrecompiledLoader(), this.nunjucksEnvironmentOptions); } else { let loaders = []; loaders.push(new FileSystemLoader(this.#getFileSystemDirs())); // These need to come after FileSystemLoader for (let loaderOptions of this.config.nunjucksLoaders) { let loader = NunjucksLib.Loader.extend(loaderOptions); loaders.push(new loader()); } this.njkEnv = new Environment(loaders, this.nunjucksEnvironmentOptions); } this.config.events.emit("eleventy.engine.njk", { nunjucks: NunjucksLib, environment: this.njkEnv, }); } setLibrary(override) { this.#setEnv(override); // Note that a new Nunjucks engine instance is created for subsequent builds // Eleventy Nunjucks is set to `cacheable` false above to opt out of Eleventy cache this.config.events.on("eleventy#templateModified", (templatePath) => { // NunjucksEnvironment: // loader.pathToNames: {'ABSOLUTE_PATH/src/_includes/components/possum-home.css': 'components/possum-home.css'} // loader.cache: { 'components/possum-home.css': [Template] } // Nunjucks stores these as Operating System native paths let absTmplPath = TemplatePath.normalizeOperatingSystemFilePath( TemplatePath.absolutePath(templatePath), ); for (let loader of this.njkEnv.loaders) { let nunjucksName = loader.pathsToNames[absTmplPath]; if (nunjucksName) { debug( "Match found in Nunjucks cache via templateModified for %o, clearing this entry", templatePath, ); delete loader.pathsToNames[absTmplPath]; delete loader.cache[nunjucksName]; } } // Behavior prior to v3.1.0-alpha.1: // this.njkEnv.invalidateCache(); }); this.setEngineLib(this.njkEnv, Boolean(this.config.libraryOverrides.njk)); this.addFilters(this.config.nunjucksFilters); this.addFilters(this.config.nunjucksAsyncFilters, true); // TODO these all go to the same place (addTag), add warnings for overwrites // TODO(zachleat): variableName should work with quotes or without quotes (same as {% set %}) // This was changed to be an async function in v4 but notably previous versions of synchronous paired shortcodes used CallExtensionAsync this.addPairedShortcode( "setAsync", async function (content, variableName) { this.ctx[variableName] = content; return ""; }, true, ); this.addCustomTags(this.config.nunjucksTags); this.addAllShortcodes(this.config.nunjucksShortcodes); this.addAllShortcodes(this.config.nunjucksAsyncShortcodes, true); this.addAllPairedShortcodes(this.config.nunjucksPairedShortcodes); this.addAllPairedShortcodes(this.config.nunjucksAsyncPairedShortcodes, true); this.addGlobals(this.config.nunjucksGlobals); } addFilters(filters, isAsync) { for (let name in filters) { this.njkEnv.addFilter(name, Nunjucks.wrapFilter(name, filters[name]), isAsync); } } static wrapFilter(name, fn) { return function (...args) { try { augmentObject(this, { source: this.ctx, lazy: false, // context.env?.opts.throwOnUndefined, }); return fn.call(this, ...args); } catch (e) { throw new EleventyNunjucksError( `Error in Nunjucks Filter \`${name}\`${this.page ? ` (${this.page.inputPath})` : ""}`, e, ); } }; } // Shortcodes static normalizeContext(context) { let obj = {}; if (context.ctx) { obj.ctx = context.ctx; obj.env = context.env; augmentObject(obj, { source: context.ctx, lazy: false, // context.env?.opts.throwOnUndefined, }); } return obj; } addCustomTags(tags) { for (let name in tags) { this.addTag(name, tags[name]); } } addTag(name, tagFn) { let tagObj; if (typeof tagFn === "function") { tagObj = tagFn(NunjucksLib, this.njkEnv); } else { throw new Error( "Nunjucks.addTag expects a callback function to be passed in: addTag(name, function(nunjucksEngine) {})", ); } this.njkEnv.addExtension(name, tagObj); } addGlobals(globals) { for (let name in globals) { this.addGlobal(name, globals[name]); } } addGlobal(name, globalFn) { this.njkEnv.addGlobal(name, globalFn); } addAllShortcodes(shortcodes, isAsync = false) { for (let name in shortcodes) { this.addShortcode(name, shortcodes[name], isAsync); } } addAllPairedShortcodes(shortcodes, isAsync = false) { for (let name in shortcodes) { this.addPairedShortcode(name, shortcodes[name], isAsync); } } _getShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) { return function ShortcodeFunction() { this.tags = [shortcodeName]; this.parse = function (parser, nodes) { let args; let tok = parser.nextToken(); args = parser.parseSignature(true, true); // Nunjucks bug with non-paired custom tags bug still exists even // though this issue is closed. Works fine for paired. // https://github.com/mozilla/nunjucks/issues/158 // https://github.com/11ty/eleventy/issues/372 if (args.children.length === 0) { // Changed from an empty string to an empty NodeList // https://github.com/11ty/eleventy/issues/3788 args.addChild(new nodes.NodeList()); } parser.advanceAfterBlockEnd(tok.value); if (isAsync) { return new nodes.CallExtensionAsync(this, "run", args); } return new nodes.CallExtension(this, "run", args); }; this.run = function (...args) { let resolve; if (isAsync) { resolve = args.pop(); } let [context, ...argArray] = args; if (isAsync) { let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray); // #3286 error messaging when the shortcode is not a promise if (!ret?.then) { resolve( new EleventyNunjucksError( `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`, ), ); } ret.then( function (returnValue) { resolve(null, new NunjucksLib.runtime.SafeString("" + returnValue)); }, function (e) { resolve( new EleventyNunjucksError(`Error with Nunjucks shortcode \`${shortcodeName}\``, e), ); }, ); } else { try { let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray); return new NunjucksLib.runtime.SafeString("" + ret); } catch (e) { throw new EleventyNunjucksError( `Error with Nunjucks shortcode \`${shortcodeName}\``, e, ); } } }; }; } _getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) { return function PairedShortcodeFunction() { this.tags = [shortcodeName]; if (isAsync) { this.parse = function (parser, nodes) { var tok = parser.nextToken(); var args = parser.parseSignature(true, true); parser.advanceAfterBlockEnd(tok.value); var body = parser.parseUntilBlocks("end" + shortcodeName); parser.advanceAfterBlockEnd(); return new nodes.CallExtensionAsync(this, "run", args, [body]); }; this.run = function (...args) { let resolve = args.pop(); let body = args.pop(); let [context, ...argArray] = args; body(function (e, bodyContent) { if (e) { resolve( new EleventyNunjucksError( `Error with Nunjucks paired shortcode \`${shortcodeName}\``, e, ), ); } let ret = shortcodeFn.call( Nunjucks.normalizeContext(context), bodyContent, ...argArray, ); // #3286 error messaging when the shortcode is not a promise if (!ret?.then) { throw new EleventyNunjucksError( `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`, ); } ret.then( function (returnValue) { resolve(null, new NunjucksLib.runtime.SafeString(returnValue)); }, function (e) { resolve( new EleventyNunjucksError( `Error with Nunjucks paired shortcode \`${shortcodeName}\``, e, ), ); }, ); }); }; } else { this.parse = function (parser, nodes) { var tok = parser.nextToken(); var args = parser.parseSignature(true, true); parser.advanceAfterBlockEnd(tok.value); var body = parser.parseUntilBlocks("end" + shortcodeName); parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, "run", args, [body]); }; this.run = function (...args) { let body = args.pop(); let [context, ...argArray] = args; let bodyContent = body(); try { return new NunjucksLib.runtime.SafeString( shortcodeFn.call(Nunjucks.normalizeContext(context), bodyContent, ...argArray), ); } catch (e) { throw new EleventyNunjucksError( `Error with Nunjucks paired shortcode \`${shortcodeName}\``, e, ); } }; } }; } addShortcode(shortcodeName, shortcodeFn, isAsync = false) { let fn = this._getShortcodeFn(shortcodeName, shortcodeFn, isAsync); this.njkEnv.addExtension(shortcodeName, new fn()); } addPairedShortcode(shortcodeName, shortcodeFn, isAsync = false) { let fn = this._getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync); this.njkEnv.addExtension(shortcodeName, new fn()); } // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink) permalinkNeedsCompilation(str) { if (typeof str === "string") { return this.needsCompilation(str); } } needsCompilation(str) { // Defend against syntax customisations: // https://mozilla.github.io/nunjucks/api.html#customizing-syntax let optsTags = this.njkEnv.opts.tags || {}; let blockStart = optsTags.blockStart || "{%"; let variableStart = optsTags.variableStart || "{{"; let commentStart = optsTags.variableStart || "{#"; return ( str.indexOf(blockStart) !== -1 || str.indexOf(variableStart) !== -1 || str.indexOf(commentStart) !== -1 ); } _getParseExtensions() { if (this._parseExtensions) { return this._parseExtensions; } // add extensions so the parser knows about our custom tags/blocks let ext = []; for (let name in this.config.nunjucksTags) { let fn = this._getShortcodeFn(name, () => {}); ext.push(new fn()); } for (let name in this.config.nunjucksShortcodes) { let fn = this._getShortcodeFn(name, () => {}); ext.push(new fn()); } for (let name in this.config.nunjucksAsyncShortcodes) { let fn = this._getShortcodeFn(name, () => {}, true); ext.push(new fn()); } for (let name in this.config.nunjucksPairedShortcodes) { let fn = this._getPairedShortcodeFn(name, () => {}); ext.push(new fn()); } for (let name in this.config.nunjucksAsyncPairedShortcodes) { let fn = this._getPairedShortcodeFn(name, () => {}, true); ext.push(new fn()); } this._parseExtensions = ext; return ext; } /* Outputs an Array of lodash get selectors */ parseForSymbols(str) { if (!str) { return []; } const { parser, nodes } = NunjucksLib; let obj = parser.parse(str, this._getParseExtensions()); if (!obj) { return []; } let linesplit = str.split("\n"); let values = obj.findAll(nodes.Value); let symbols = obj.findAll(nodes.Symbol).map((entry) => { let name = [entry.value]; let nestedIndex = -1; for (let val of values) { if (nestedIndex > -1) { /* deep.object.syntax */ if (linesplit[val.lineno].charAt(nestedIndex) === ".") { name.push(val.value); nestedIndex += val.value.length + 1; } else { nestedIndex = -1; } } else if ( val.lineno === entry.lineno && val.colno === entry.colno && val.value === entry.value ) { nestedIndex = entry.colno + entry.value.length; } } return name.join("."); }); let uniqueSymbols = Array.from(new Set(symbols)); return uniqueSymbols; } async compile(str, inputPath) { let tmpl; // *All* templates are precompiled to avoid runtime eval if (this._usingPrecompiled) { tmpl = this.njkEnv.getTemplate(str, true); } else if (!inputPath || inputPath === "njk" || inputPath === "md") { // Template(content, environment, path, eagerCompile) tmpl = new Template(str, this.njkEnv, null, false); } else { // Template(content, environment, path, eagerCompile) tmpl = new Template(str, this.njkEnv, inputPath, false); } return function (data) { let { promise, resolve, reject } = withResolvers(); tmpl.render(data, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); return promise; }; } } ================================================ FILE: src/Engines/TemplateEngine.js ================================================ import debugUtil from "debug"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; class TemplateEngineConfigError extends EleventyBaseError {} const debug = debugUtil("Eleventy:TemplateEngine"); const AMENDED_INSTANCES = new Set(); export default class TemplateEngine { #extensionMap; #engineManager; #benchmarks; constructor(name, eleventyConfig) { this.name = name; this.engineLib = null; if (!eleventyConfig) { throw new TemplateEngineConfigError("Missing `eleventyConfig` argument."); } this.eleventyConfig = eleventyConfig; } get cacheable() { return false; } get dirs() { return this.eleventyConfig.directories; } get inputDir() { return this.dirs.input; } get includesDir() { return this.dirs.includes; } get config() { if (this.eleventyConfig.constructor.name !== "TemplateConfig") { throw new Error("Expecting a TemplateConfig instance."); } return this.eleventyConfig.getConfig(); } get benchmarks() { if (!this.#benchmarks) { this.#benchmarks = { aggregate: this.config.benchmarkManager.get("Aggregate"), }; } return this.#benchmarks; } get engineManager() { return this.#engineManager; } set engineManager(manager) { this.#engineManager = manager; } get extensionMap() { if (!this.#extensionMap) { throw new Error("Internal error: missing `extensionMap` in TemplateEngine."); } return this.#extensionMap; } set extensionMap(map) { this.#extensionMap = map; } get extensions() { if (!this._extensions) { this._extensions = this.extensionMap.getExtensionsFromKey(this.name); } return this._extensions; } get extensionEntries() { if (!this._extensionEntries) { this._extensionEntries = this.extensionMap.getExtensionEntriesFromKey(this.name); } return this._extensionEntries; } getName() { return this.name; } // Backwards compat getIncludesDir() { return this.includesDir; } /** * @protected */ setEngineLib(engineLib, isOverrideViaSetLibrary = false) { this.engineLib = engineLib; // Run engine amendments (via issue #2438) // Issue #3816: this isn’t ideal but there is no other way to reset a markdown instance if it was also overridden by addLibrary if (AMENDED_INSTANCES.has(engineLib)) { return; } if (isOverrideViaSetLibrary) { AMENDED_INSTANCES.add(engineLib); } debug( "Running amendLibrary for %o (number of amendments: %o)", this.name, this.config.libraryAmendments[this.name]?.length, ); for (let amendment of this.config.libraryAmendments[this.name] || []) { // TODO it’d be nice if this were async friendly amendment(engineLib); } } getEngineLib() { return this.engineLib; } async _testRender(str, data) { // @ts-ignore let fn = await this.compile(str); return fn(data); } useJavaScriptImport() { return false; } // JavaScript files defer to the module loader rather than read the files to strings needsToReadFileContents() { return true; } getExtraDataFromFile() { return {}; } getCompileCacheKey(str, inputPath) { // Changing to use inputPath and contents, using only file contents (`str`) caused issues when two // different files had identical content (2.0.0-canary.16) // Caches are now segmented based on inputPath so using inputPath here is superfluous (2.0.0-canary.19) // But we do want a non-falsy value here even if `str` is an empty string. return { useCache: true, key: inputPath + str, }; } get defaultTemplateFileExtension() { return "html"; } // Whether or not to wrap in Eleventy layouts useLayouts() { return true; } /** @returns {boolean|undefined} */ permalinkNeedsCompilation(str) { return this.needsCompilation(); } // whether or not compile is needed or can we return the plaintext? needsCompilation(str) { return true; } /** * Make sure compile is implemented downstream. * @abstract * @return {Promise} */ async compile() { throw new Error("compile() must be implemented by engine"); } // See https://v3.11ty.dev/docs/watch-serve/#watch-javascript-dependencies static shouldSpiderJavaScriptDependencies() { return false; } hasDependencies(inputPath) { if (this.config.uses.getDependencies(inputPath) === false) { return false; } return true; } isFileRelevantTo(inputPath, comparisonFile) { return this.config.uses.isFileRelevantTo(inputPath, comparisonFile); } } ================================================ FILE: src/Engines/TemplateEngineManager.js ================================================ import debugUtil from "debug"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; const debug = debugUtil("Eleventy:TemplateEngineManager"); class TemplateEngineManager { #CustomEngine; constructor(eleventyConfig) { if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") { throw new EleventyBaseError("Missing or invalid `config` argument."); } this.eleventyConfig = eleventyConfig; this.engineCache = {}; this.importCache = {}; } get config() { return this.eleventyConfig.getConfig(); } static isAlias(entry) { if (entry.aliasKey) { return true; } return entry.key !== entry.extension; } static isSimpleAlias(entry) { if (!this.isAlias(entry)) { return false; } // has keys other than key, extension, and aliasKey return ( Object.keys(entry).some((key) => { return key !== "key" && key !== "extension" && key !== "aliasKey"; }) === false ); } get keyToClassNameMap() { if (!this._keyToClassNameMap) { this._keyToClassNameMap = { md: "Markdown", html: "Html", njk: "Nunjucks", liquid: "Liquid", "11ty.js": "JavaScript", }; // Custom entries *can* overwrite default entries above if ("extensionMap" in this.config) { for (let entry of this.config.extensionMap) { // either the key does not already exist or it is not a simple alias and is an override: https://v3.11ty.dev/docs/languages/custom/#overriding-an-existing-template-language let existingTarget = this._keyToClassNameMap[entry.key]; let isAlias = TemplateEngineManager.isAlias(entry); if (!existingTarget && isAlias) { throw new Error( `An attempt to alias ${entry.aliasKey} to ${entry.key} was made, but ${entry.key} is not a recognized template syntax.`, ); } if (isAlias) { // only `key` and `extension`, not `compile` or other options if (!TemplateEngineManager.isSimpleAlias(entry)) { this._keyToClassNameMap[entry.aliasKey] = "Custom"; } else { this._keyToClassNameMap[entry.aliasKey] = this._keyToClassNameMap[entry.key]; } } else { // not an alias, so `key` and `extension` are the same here. // *can* override a built-in extension! this._keyToClassNameMap[entry.key] = "Custom"; } } } } return this._keyToClassNameMap; } reset() { this.engineCache = {}; } getClassNameFromTemplateKey(key) { return this.keyToClassNameMap[key]; } hasEngine(name) { return !!this.getClassNameFromTemplateKey(name); } async getEngineClassByExtension(extension) { if (this.config.extensionMapClasses[extension]) { return this.config.extensionMapClasses[extension]; } if (this.importCache[extension]) { return this.importCache[extension]; } let promise; // We include these as raw strings (and not more readable variables) so they’re parsed by a bundler. if (extension === "md") { promise = import("../Adapters/Engines/Markdown.js").then((mod) => mod.default); } else if (extension === "html") { promise = import("./Html.js").then((mod) => mod.default); } else if (extension === "njk") { promise = import("../Adapters/Engines/Nunjucks.js").then((mod) => mod.default); } else if (extension === "liquid") { promise = import("../Adapters/Engines/Liquid.js").then((mod) => mod.default); } else if (extension === "11ty.js") { // ~4KB cost, fine to keep promise = import("./JavaScript.js").then((mod) => mod.default); } else { promise = this.getCustomEngineClass(); } if (promise) { this.importCache[extension] = promise; } else { throw new Error("Missing engine for file extension: " + extension); } return promise; } async getCustomEngineClass() { if (!this.#CustomEngine) { this.#CustomEngine = import("./Custom.js").then((mod) => mod.default); } return this.#CustomEngine; } async #getEngine(name, extensionMap) { let cls = await this.getEngineClassByExtension(name); if (!cls) { throw new Error(`Missing engine for ${name}. Do you need to use eleventyConfig.addEngine?`); } let instance = new cls(name, this.eleventyConfig); instance.extensionMap = extensionMap; instance.engineManager = this; let extensionEntry = extensionMap.getExtensionEntry(name); // Override a built-in extension (md => md) // If provided a "Custom" engine using addExtension, but that engine's instance is *not* custom, // The user must be overriding a built-in engine i.e. addExtension('md', { ...overrideBehavior }) let className = this.getClassNameFromTemplateKey(name); if (className === "Custom" && instance.constructor.name !== "CustomEngine") { let CustomEngine = await this.getCustomEngineClass(); let overrideCustomEngine = new CustomEngine(name, this.eleventyConfig); // Keep track of the "default" engine 11ty would normally use // This allows the user to access the default engine in their override overrideCustomEngine.setDefaultEngine(instance); instance = overrideCustomEngine; // Alias to a built-in extension (11ty.tsx => 11ty.js) } else if ( instance.constructor.name === "CustomEngine" && TemplateEngineManager.isAlias(extensionEntry) ) { // add defaultRenderer for complex aliases with their own compile functions. let originalEngineInstance = await this.getEngine(extensionEntry.key, extensionMap); instance.setDefaultEngine(originalEngineInstance); } return instance; } isEngineRemovedFromCore(name) { return ["ejs", "hbs", "mustache", "haml", "pug"].includes(name) && !this.hasEngine(name); } async getEngine(name, extensionMap) { // Bundled engine deprecation if (this.isEngineRemovedFromCore(name)) { throw new Error( `Per the 11ty Community Survey (2023), the "${name}" template language was moved from core to an officially supported plugin in v3.0. These plugins live here: https://github.com/11ty/eleventy-plugin-template-languages and are documented on their respective template language docs at https://v3.11ty.dev/docs/languages/ You are also empowered to implement *any* template language yourself using https://v3.11ty.dev/docs/languages/custom/`, ); } if (!this.hasEngine(name)) { throw new Error(`Template Engine ${name} does not exist in getEngine()`); } // TODO these cached engines should be based on extensions not name, then we can remove the error in // "Double override (not aliases) throws an error" test in TemplateRenderCustomTest.js if (!this.engineCache[name]) { debug("Engine cache miss %o (should only happen once per engine type)", name); // Make sure cache key is based on name and not path // Custom class is used for all plugins, cache once per plugin this.engineCache[name] = this.#getEngine(name, extensionMap); } return this.engineCache[name]; } } export default TemplateEngineManager; ================================================ FILE: src/Engines/Util/ContextAugmenter.js ================================================ const DATA_KEYS = ["page", "eleventy"]; function augmentFunction(fn, options = {}) { let t = typeof fn; if (t !== "function") { throw new Error( "Invalid type passed to `augmentFunction`. A function was expected and received: " + t, ); } /** @this {object} */ return function (...args) { let context = augmentObject(this || {}, options); return fn.call(context, ...args); }; } function augmentObject(targetObject, options = {}) { options = Object.assign( { source: undefined, // where to copy from overwrite: true, lazy: false, // lazily fetch the property // getter: function() {}, }, options, ); for (let key of DATA_KEYS) { // Skip if overwrite: false and prop already exists on target if (!options.overwrite && targetObject[key]) { continue; } if (options.lazy) { let value; if (typeof options.getter == "function") { value = () => options.getter(key, options.source); } else { value = () => options.source?.[key]; } // lazy getter important for Liquid strictVariables support Object.defineProperty(targetObject, key, { writable: true, configurable: true, enumerable: true, value, }); } else { let value; if (typeof options.getter == "function") { value = options.getter(key, options.source); } else { value = options.source?.[key]; } if (value) { targetObject[key] = value; } } } return targetObject; } export { DATA_KEYS as augmentKeys, augmentFunction, augmentObject }; ================================================ FILE: src/Errors/DuplicatePermalinkOutputError.js ================================================ import EleventyBaseError from "./EleventyBaseError.js"; class DuplicatePermalinkOutputError extends EleventyBaseError { get removeDuplicateErrorStringFromOutput() { return true; } } export default DuplicatePermalinkOutputError; ================================================ FILE: src/Errors/EleventyBaseError.js ================================================ /** * This class serves as basis for all Eleventy-specific errors. * @ignore */ class EleventyBaseError extends Error { /** * @param {string} message - The error message to display. * @param {unknown} [originalError] - The original error caught. */ constructor(message, originalError) { if (originalError) { // @ts-ignore super(message, { cause: originalError }); } else { super(message); } this.name = this.constructor.name; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } if (originalError) { this.originalError = originalError; } } } export default EleventyBaseError; ================================================ FILE: src/Errors/EleventyErrorHandler.js ================================================ import debugUtil from "debug"; import { inspect } from "../Adapters/Packages/inspect.js"; import ConsoleLogger from "../Util/ConsoleLogger.js"; import EleventyErrorUtil from "./EleventyErrorUtil.js"; const debug = debugUtil("Eleventy:EleventyErrorHandler"); class EleventyErrorHandler { constructor() { this._isVerbose = true; } get isVerbose() { return this._isVerbose; } set isVerbose(verbose) { this._isVerbose = !!verbose; this.logger.isVerbose = !!verbose; } get logger() { if (!this._logger) { this._logger = new ConsoleLogger(); this._logger.isVerbose = this.isVerbose; } return this._logger; } set logger(logger) { this._logger = logger; } warn(e, msg) { this.log(e, "warn", "yellow", undefined, [`${msg}:`]); } fatal(e, msg) { this.error(e, msg); process.exitCode = 1; } once(type, e, msg) { if (e.__errorAlreadyLogged) { return; } this[type || "error"](e, msg); Object.defineProperty(e, "__errorAlreadyLogged", { value: true, }); } error(e, msg) { this.log(e, "error", "red", true, [`${msg}:`]); } static getTotalErrorCount(e) { let totalErrorCount = 0; let errorCountRef = e; while (errorCountRef) { totalErrorCount++; errorCountRef = errorCountRef.originalError; } return totalErrorCount; } //https://nodejs.org/api/process.html log(e, type = "log", chalkColor = "", forceToConsole = false, messages = []) { if (process.env.DEBUG) { debug("Full error object: %o", inspect(e)); } let showStack = true; if (e.skipOriginalStack) { // Don’t show the full error stack trace showStack = false; } let totalErrorCount = EleventyErrorHandler.getTotalErrorCount(e); let ref = e; let index = 1; let debugs = []; while (ref) { let nextRef = ref.originalError; // Unwrap cause from error and assign it to what Eleventy expects if (nextRef?.cause) { nextRef.originalError = nextRef.cause?.originalError ?? nextRef?.cause; } if (!nextRef && EleventyErrorUtil.hasEmbeddedError(ref.message)) { nextRef = EleventyErrorUtil.deconvertErrorToObject(ref); } if (nextRef?.skipOriginalStack) { showStack = false; } messages.push( `${totalErrorCount > 1 ? `${index}. ` : ""}${( EleventyErrorUtil.cleanMessage(ref.message) || "(No error message provided)" ).trim()}${ref.name !== "Error" ? ` (via ${ref.name})` : ""}`, ); if (process.env.DEBUG) { debugs.push(`(${type} stack): ${ref.stack}`); } else if (!nextRef) { // last error in the loop // remove duplicate error messages if the stack contains the original message output above let stackStr = ref.stack || ""; if (e.removeDuplicateErrorStringFromOutput) { stackStr = stackStr.replace( `${ref.name}: ${ref.message}`, "(Repeated output has been truncated…)", ); } if (showStack) { messages.push("\nOriginal error stack trace: " + stackStr); } } ref = nextRef; index++; } if (messages.length) { this.logger.message(messages.join("\n"), type, chalkColor, forceToConsole); } if (debugs.length > 0) { for (let msg of debugs) { debug(msg); } } } } export { EleventyErrorHandler }; ================================================ FILE: src/Errors/EleventyErrorUtil.js ================================================ import TemplateContentPrematureUseError from "./TemplateContentPrematureUseError.js"; /* Hack to workaround the variety of error handling schemes in template languages */ class EleventyErrorUtil { static get prefix() { return ">>>>>11ty>>>>>"; } static get suffix() { return "<<<<<11ty<<<<<"; } static hasEmbeddedError(msg) { if (!msg) { return false; } return msg.includes(EleventyErrorUtil.prefix) && msg.includes(EleventyErrorUtil.suffix); } static cleanMessage(msg) { if (!msg) { return ""; } if (!EleventyErrorUtil.hasEmbeddedError(msg)) { return "" + msg; } return msg.slice(0, Math.max(0, msg.indexOf(EleventyErrorUtil.prefix))); } static deconvertErrorToObject(error) { if (!error || !error.message) { throw new Error(`Could not convert error object from: ${error}`); } if (!EleventyErrorUtil.hasEmbeddedError(error.message)) { return error; } let msg = error.message; let objectString = msg.substring( msg.indexOf(EleventyErrorUtil.prefix) + EleventyErrorUtil.prefix.length, msg.lastIndexOf(EleventyErrorUtil.suffix), ); let obj = JSON.parse(objectString); obj.name = error.name; return obj; } // pass an error through a random template engine’s error handling unscathed static convertErrorToString(error) { return ( EleventyErrorUtil.prefix + JSON.stringify({ message: error.message, stack: error.stack }) + EleventyErrorUtil.suffix ); } static isPrematureTemplateContentError(e) { // TODO the rest of the template engines return ( e instanceof TemplateContentPrematureUseError || e?.cause instanceof TemplateContentPrematureUseError || // Custom (per Node-convention) ["RenderError", "UndefinedVariableError"].includes(e?.originalError?.name) && e?.originalError?.originalError instanceof TemplateContentPrematureUseError || // Liquid e?.message?.includes("TemplateContentPrematureUseError") // Nunjucks ); } } export default EleventyErrorUtil; ================================================ FILE: src/Errors/TemplateContentPrematureUseError.js ================================================ import EleventyBaseError from "./EleventyBaseError.js"; class TemplateContentPrematureUseError extends EleventyBaseError {} export default TemplateContentPrematureUseError; ================================================ FILE: src/Errors/TemplateContentUnrenderedTemplateError.js ================================================ import EleventyBaseError from "./EleventyBaseError.js"; class TemplateContentUnrenderedTemplateError extends EleventyBaseError {} export default TemplateContentUnrenderedTemplateError; ================================================ FILE: src/Errors/UsingCircularTemplateContentReferenceError.js ================================================ import EleventyBaseError from "./EleventyBaseError.js"; class UsingCircularTemplateContentReferenceError extends EleventyBaseError {} export default UsingCircularTemplateContentReferenceError; ================================================ FILE: src/EventBus.js ================================================ import debugUtil from "debug"; import EventEmitter from "./Util/AsyncEventEmitter.js"; const debug = debugUtil("Eleventy:EventBus"); /** * @module 11ty/eleventy/EventBus * @ignore */ debug("Setting up global EventBus."); /** * Provides a global event bus that modules deep down in the stack can * subscribe to from a global singleton for decoupled pub/sub. * @type {module:11ty/eleventy/Util/AsyncEventEmitter~AsyncEventEmitter} */ let bus = new EventEmitter(); bus.setMaxListeners(100); // defaults to 10 debug("EventBus max listener count: %o", bus.getMaxListeners()); export default bus; ================================================ FILE: src/FileSystemSearch.js ================================================ import { glob } from "tinyglobby"; import { TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import GlobRemap from "./Util/GlobRemap.js"; import { isGlobMatch } from "./Util/GlobMatcher.js"; const debug = debugUtil("Eleventy:FileSystemSearch"); class FileSystemSearch { constructor() { this.inputs = {}; this.outputs = {}; this.promises = {}; this.count = 0; } getCacheKey(key, globs, options) { if (Array.isArray(globs)) { globs = globs.sort(); } return key + JSON.stringify(globs) + JSON.stringify(options); } // returns a promise search(key, globs, options = {}) { debug("Glob search (%o) searching for: %o", key, globs); if (!Array.isArray(globs)) { globs = [globs]; } // Strip leading slashes from everything! globs = globs.map((entry) => { return TemplatePath.stripLeadingDotSlash(entry); }); let cwd = GlobRemap.getCwd(globs); if (cwd) { options.cwd = cwd; } if (options.ignore && Array.isArray(options.ignore)) { options.ignore = options.ignore.map((entry) => { entry = TemplatePath.stripLeadingDotSlash(entry); return GlobRemap.remapInput(entry, cwd); }); debug("Glob search (%o) ignoring: %o", key, options.ignore); } let cacheKey = this.getCacheKey(key, globs, options); // Only after the promise has resolved if (this.outputs[cacheKey]) { return Array.from(this.outputs[cacheKey]); } if (!this.promises[cacheKey]) { this.inputs[cacheKey] = { input: globs, options, }; this.count++; globs = globs.map((entry) => { if (cwd && entry.startsWith(cwd)) { return GlobRemap.remapInput(entry, cwd); } return entry; }); options.caseSensitiveMatch ??= false; // insensitive options.dot ??= true; this.promises[cacheKey] = glob(globs, options).then((results) => { this.outputs[cacheKey] = new Set( results.map((entry) => { let remapped = GlobRemap.remapOutput(entry, options.cwd); return TemplatePath.standardizeFilePath(remapped); }), ); return Array.from(this.outputs[cacheKey]); }); } // may be an unresolved promise return this.promises[cacheKey]; } _modify(path, setOperation) { path = TemplatePath.stripLeadingDotSlash(path); let normalized = TemplatePath.standardizeFilePath(path); for (let key in this.inputs) { let { input, options } = this.inputs[key]; if ( isGlobMatch(path, input, { ignore: options.ignore, }) ) { this.outputs[key][setOperation](normalized); } } } add(path) { this._modify(path, "add"); } delete(path) { this._modify(path, "delete"); } // Issue #3859 get rid of chokidar globs // getAllOutputFiles() { // return Object.values(this.outputs).map(set => Array.from(set)).flat(); // } } export default FileSystemSearch; ================================================ FILE: src/Filters/GetCollectionItem.js ================================================ export default function getCollectionItem(collection, page, modifier = 0) { let j = 0; let index; for (let item of collection) { if ( item.inputPath === page.inputPath && (item.outputPath === page.outputPath || item.url === page.url) ) { index = j; break; } j++; } if (index !== undefined && collection?.length) { if (index + modifier >= 0 && index + modifier < collection.length) { return collection[index + modifier]; } } } ================================================ FILE: src/Filters/GetCollectionItemIndex.js ================================================ // TODO locale-friendly, see GetLocaleCollectionItem.js) export default function getCollectionItemIndex(collection, page) { if (!page) { page = this.page; } let j = 0; for (let item of collection) { if ( item.inputPath === page.inputPath && (item.outputPath === page.outputPath || item.url === page.url) ) { return j; } j++; } } ================================================ FILE: src/Filters/GetLocaleCollectionItem.js ================================================ import getCollectionItem from "./GetCollectionItem.js"; // Work with I18n Plugin src/Plugins/I18nPlugin.js to retrieve root pages (not i18n pages) function resolveRootPage(config, pageOverride, languageCode) { let localeFilter = config.getFilter("locale_page"); if (!localeFilter || typeof localeFilter !== "function") { return pageOverride; } // returns root/default-language `page` object return localeFilter.call(this, pageOverride, languageCode); } function getLocaleCollectionItem(config, collection, pageOverride, langCode, indexModifier = 0) { if (!langCode) { // if page.lang exists (2.0.0-canary.14 and i18n plugin added, use page language) if (this.page.lang) { langCode = this.page.lang; } else { return getCollectionItem(collection, pageOverride || this.page, indexModifier); } } let rootPage = resolveRootPage.call(this, config, pageOverride); // implied current page, default language let modifiedRootItem = getCollectionItem(collection, rootPage, indexModifier); if (!modifiedRootItem) { return; // no root item exists for the previous/next page } // Resolve modified root `page` back to locale `page` // This will return a non localized version of the page as a fallback let modifiedLocalePage = resolveRootPage.call(this, config, modifiedRootItem.data.page, langCode); // already localized (or default language) if (!("__locale_page_resolved" in modifiedLocalePage)) { return modifiedRootItem; } // find the modified locale `page` again in `collections.all` let all = this.collections?.all || this.ctx?.collections?.all || this.context?.environments?.collections?.all || []; return getCollectionItem(all, modifiedLocalePage, 0); } export default getLocaleCollectionItem; ================================================ FILE: src/Filters/Url.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import { isValidUrl } from "../Util/UrlUtil.js"; // Note: This filter is used in the Eleventy Navigation plugin in versions prior to 0.3.4 export default function (url, pathPrefix) { // work with undefined url = url || ""; if (isValidUrl(url) || (url.startsWith("//") && url !== "//")) { return url; } if (pathPrefix === undefined || typeof pathPrefix !== "string") { // When you retrieve this with config.getFilter("url") it // grabs the pathPrefix argument from your config for you (see defaultConfig.js) throw new Error("pathPrefix (String) is required in the `url` filter."); } let normUrl = TemplatePath.normalizeUrlPath(url); let normRootDir = TemplatePath.normalizeUrlPath("/", pathPrefix); let normFull = TemplatePath.normalizeUrlPath("/", pathPrefix, url); let isRootDirTrailingSlash = normRootDir.length && normRootDir.charAt(normRootDir.length - 1) === "/"; // minor difference with straight `normalize`, "" resolves to root dir and not "." // minor difference with straight `normalize`, "/" resolves to root dir if (normUrl === "/" || normUrl === normRootDir) { return normRootDir + (!isRootDirTrailingSlash ? "/" : ""); } else if (normUrl.indexOf("/") === 0) { return normFull; } return normUrl; } ================================================ FILE: src/GlobalDependencyMap.js ================================================ import debugUtil from "debug"; import { TemplatePath } from "@11ty/eleventy-utils"; import JavaScriptDependencies from "./Util/JavaScriptDependencies.js"; import PathNormalizer from "./Util/PathNormalizer.js"; import { TemplateDepGraph } from "./Util/TemplateDepGraph.js"; const debug = debugUtil("Eleventy:Dependencies"); class GlobalDependencyMap { // dependency-graph requires these keys to be alphabetic strings static LAYOUT_KEY = "layout"; static COLLECTION_PREFIX = "__collection:"; // must match TemplateDepGraph key #map; #templateConfig; #cachedUserConfigurationCollectionApiNames; static isCollection(entry) { return entry.startsWith(this.COLLECTION_PREFIX); } static getTagName(entry) { if (this.isCollection(entry)) { return entry.slice(this.COLLECTION_PREFIX.length); } } static getCollectionKeyForEntry(entry) { return `${GlobalDependencyMap.COLLECTION_PREFIX}${entry}`; } reset() { this.#map = undefined; } setIsEsm(isEsm) { this.isEsm = isEsm; } setTemplateConfig(templateConfig) { this.#templateConfig = templateConfig; // These have leading dot slashes, but so do the paths from Eleventy this.#templateConfig.userConfig.events.once("eleventy.layouts", async (layouts) => { await this.addLayoutsToMap(layouts); }); } get userConfigurationCollectionApiNames() { if (this.#cachedUserConfigurationCollectionApiNames) { return this.#cachedUserConfigurationCollectionApiNames; } return Object.keys(this.#templateConfig.userConfig.getCollections()) || []; } initializeUserConfigurationApiCollections() { this.addCollectionApiNames(this.userConfigurationCollectionApiNames); } // For Testing setCollectionApiNames(names = []) { this.#cachedUserConfigurationCollectionApiNames = names; } addCollectionApiNames(names = []) { if (!names || names.length === 0) { return; } for (let collectionName of names) { this.map.addConfigCollectionName(collectionName); } } filterOutLayouts(nodes = []) { return nodes.filter((node) => { if (GlobalDependencyMap.isLayoutNode(this.map, node)) { return false; } return true; }); } filterOutCollections(nodes = []) { return nodes.filter((node) => !node.startsWith(GlobalDependencyMap.COLLECTION_PREFIX)); } static removeLayoutNodes(graph, nodeList) { return nodeList.filter((node) => { if (this.isLayoutNode(graph, node)) { return false; } return true; }); } removeLayoutNodes(normalizedLayouts) { let nodes = this.map.overallOrder(); for (let node of nodes) { if (!GlobalDependencyMap.isLayoutNode(this.map, node)) { continue; } // previous layout is not in the new layout map (no templates are using it) if (!normalizedLayouts[node]) { this.map.removeNode(node); } // important: if the layout map changed to have different templates (but was not removed) // this is already handled by `resetNode` called via TemplateMap } } // Eleventy Layouts don’t show up in the dependency graph, so we handle those separately async addLayoutsToMap(layouts) { let normalizedLayouts = this.normalizeLayoutsObject(layouts); // Clear out any previous layout relationships to make way for the new ones this.removeLayoutNodes(normalizedLayouts); for (let layout in normalizedLayouts) { // We add this pre-emptively to add the `layout` data if (!this.map.hasNode(layout)) { this.map.addNode(layout, { type: GlobalDependencyMap.LAYOUT_KEY, }); } else { this.map.setNodeData(layout, { type: GlobalDependencyMap.LAYOUT_KEY, }); } // Potential improvement: only add the first template in the chain for a template and manage any upstream layouts by their own relationships for (let pageTemplate of normalizedLayouts[layout]) { this.addDependency(pageTemplate, [layout]); } if (this.#templateConfig?.shouldSpiderJavaScriptDependencies()) { let deps = await JavaScriptDependencies.getDependencies([layout], this.isEsm); this.addDependency(layout, deps); } } } get map() { if (!this.#map) { // this.#map = new DepGraph({ circular: true }); this.#map = new TemplateDepGraph(); } return this.#map; } set map(graph) { this.#map = graph; } normalizeNode(node) { if (!node) { return; } // TODO tests for this // Fix URL objects passed in (sass does this) if (typeof node !== "string" && "toString" in node) { node = node.toString(); } if (typeof node !== "string") { throw new Error("`addDependencies` files must be strings. Received:" + node); } return PathNormalizer.fullNormalization(node); } normalizeLayoutsObject(layouts) { let o = {}; for (let rawLayout in layouts) { let layout = this.normalizeNode(rawLayout); o[layout] = layouts[rawLayout].map((entry) => this.normalizeNode(entry)); } return o; } getDependantsFor(node) { if (!node) { return []; } node = this.normalizeNode(node); if (!this.map.hasNode(node)) { return []; } // Direct dependants and dependencies, both publish and consume from collections return this.map.directDependantsOf(node); } hasNode(node) { return this.map.hasNode(this.normalizeNode(node)); } findCollectionsRemovedFrom(node, collectionNames) { if (!this.hasNode(node)) { return new Set(); } let prevDeps = this.getDependantsFor(node) .map((entry) => GlobalDependencyMap.getTagName(entry)) .filter(Boolean); let prevDepsSet = new Set(prevDeps); let deleted = new Set(); for (let dep of prevDepsSet) { if (!collectionNames.has(dep)) { deleted.add(dep); } } return deleted; } resetNode(node) { node = this.normalizeNode(node); if (!this.map.hasNode(node)) { return; } // We don’t want to remove relationships that consume this, controlled by the upstream content // for (let dep of this.map.directDependantsOf(node)) { // this.map.removeDependency(dep, node); // } for (let dep of this.map.directDependenciesOf(node)) { this.map.removeDependency(node, dep); } } getTemplatesThatConsumeCollections(collectionNames) { let templates = new Set(); for (let name of collectionNames) { let collectionKey = GlobalDependencyMap.getCollectionKeyForEntry(name); if (!this.map.hasNode(collectionKey)) { continue; } for (let node of this.map.dependantsOf(collectionKey)) { if (!node.startsWith(GlobalDependencyMap.COLLECTION_PREFIX)) { if (!GlobalDependencyMap.isLayoutNode(this.map, node)) { templates.add(node); } } } } return templates; } static isLayoutNode(graph, node) { if (!graph.hasNode(node)) { return false; } return graph.getNodeData(node)?.type === GlobalDependencyMap.LAYOUT_KEY; } getLayoutsUsedBy(node) { node = this.normalizeNode(node); if (!this.map.hasNode(node)) { return []; } let layouts = []; // include self, if layout if (GlobalDependencyMap.isLayoutNode(this.map, node)) { layouts.push(node); } this.map.dependantsOf(node).forEach((node) => { // we only want layouts if (GlobalDependencyMap.isLayoutNode(this.map, node)) { return layouts.push(node); } }); return layouts; } // In order // Does not include original templatePaths (unless *they* are second-order relevant) getTemplatesRelevantToTemplateList(templatePaths) { let overallOrder = this.map.overallOrder(); overallOrder = this.filterOutLayouts(overallOrder); overallOrder = this.filterOutCollections(overallOrder); let relevantLookup = {}; for (let inputPath of templatePaths) { inputPath = TemplatePath.stripLeadingDotSlash(inputPath); let deps = this.getDependencies(inputPath, false); if (Array.isArray(deps)) { let paths = this.filterOutCollections(deps); for (let node of paths) { relevantLookup[node] = true; } } } return overallOrder.filter((node) => { if (relevantLookup[node]) { return true; } return false; }); } // Layouts are not relevant to compile cache and can be ignored getDependencies(node, includeLayouts = true) { node = this.normalizeNode(node); // `false` means the Node was unknown if (!this.map.hasNode(node)) { return false; } if (includeLayouts) { return this.map.dependenciesOf(node).filter(Boolean); } return GlobalDependencyMap.removeLayoutNodes(this.map, this.map.dependenciesOf(node)); } #addNode(name) { if (this.map.hasNode(name)) { return; } this.map.addNode(name); } // node arguments are already normalized #addDependency(from, toArray = []) { this.#addNode(from); // only if not already added if (!Array.isArray(toArray)) { throw new Error("Second argument to `addDependency` must be an Array."); } // debug("%o depends on %o", from, toArray); for (let to of toArray) { this.#addNode(to); // only if not already added if (from !== to) { this.map.addDependency(from, to); } } } addDependency(from, toArray = []) { this.#addDependency( this.normalizeNode(from), toArray.map((to) => this.normalizeNode(to)), ); } addNewNodeRelationships(from, consumes = [], publishes = []) { consumes = consumes.filter(Boolean); publishes = publishes.filter(Boolean); debug("%o consumes %o and publishes to %o", from, consumes, publishes); from = this.normalizeNode(from); this.map.addTemplate(from, consumes, publishes); } // Layouts are not relevant to compile cache and can be ignored hasDependency(from, to, includeLayouts) { to = this.normalizeNode(to); let deps = this.getDependencies(from, includeLayouts); // normalizes `from` if (!deps) { return false; } return deps.includes(to); } // Layouts are not relevant to compile cache and can be ignored isFileRelevantTo(fullTemplateInputPath, comparisonFile, includeLayouts) { fullTemplateInputPath = this.normalizeNode(fullTemplateInputPath); comparisonFile = this.normalizeNode(comparisonFile); // No watch/serve changed file if (!comparisonFile) { return false; } // The file that changed is the relevant file if (fullTemplateInputPath === comparisonFile) { return true; } // The file that changed is a dependency of the template // comparisonFile is used by fullTemplateInputPath if (this.hasDependency(fullTemplateInputPath, comparisonFile, includeLayouts)) { return true; } return false; } isFileUsedBy(parent, child, includeLayouts) { if (this.hasDependency(parent, child, includeLayouts)) { // child is used by parent return true; } return false; } getTemplateOrder() { let order = []; for (let entry of this.map.overallOrder()) { order.push(entry); } return order; } stringify() { return JSON.stringify(this.map, function replacer(key, value) { // Serialize internal Map objects. if (value instanceof Map) { let obj = {}; for (let [k, v] of value) { obj[k] = v; } return obj; } return value; }); } restore(persisted) { let obj = JSON.parse(persisted); let graph = new TemplateDepGraph(); // https://github.com/jriecken/dependency-graph/issues/44 // Restore top level serialized Map objects (in stringify above) for (let key in obj) { let map = graph[key]; for (let k in obj[key]) { let v = obj[key][k]; map.set(k, v); } } this.map = graph; } } export default GlobalDependencyMap; ================================================ FILE: src/LayoutCache.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import eventBus from "./EventBus.js"; // Note: this is only used for TemplateLayout right now but could be used for more // Just be careful because right now the TemplateLayout cache keys are not directly mapped to paths // So you may get collisions if you use this for other things. class LayoutCache { constructor() { this.cache = {}; this.cacheByInputPath = {}; } clear() { this.cache = {}; this.cacheByInputPath = {}; } // alias removeAll() { for (let layoutFilePath in this.cacheByInputPath) { this.remove(layoutFilePath); } this.clear(); } size() { return Object.keys(this.cacheByInputPath).length; } add(layoutTemplate) { let keys = new Set(); if (typeof layoutTemplate === "string") { throw new Error( "Invalid argument type passed to LayoutCache->add(). Should be a TemplateLayout.", ); } if ("getFullKey" in layoutTemplate) { keys.add(layoutTemplate.getFullKey()); } if ("getKey" in layoutTemplate) { // if `key` was an alias, also set to the pathed layout value too // e.g. `layout: "default"` and `layout: "default.liquid"` will both map to the same template. keys.add(layoutTemplate.getKey()); } for (let key of keys) { this.cache[key] = layoutTemplate; } // also the full template input path for use with eleventy --serve/--watch e.g. `_includes/default.liquid` (see `remove` below) let fullPath = TemplatePath.stripLeadingDotSlash(layoutTemplate.inputPath); this.cacheByInputPath[fullPath] = layoutTemplate; } has(key) { return key in this.cache; } get(key) { if (!this.has(key)) { throw new Error(`Could not find ${key} in LayoutCache.`); } return this.cache[key]; } remove(layoutFilePath) { layoutFilePath = TemplatePath.stripLeadingDotSlash(layoutFilePath); if (!this.cacheByInputPath[layoutFilePath]) { // not a layout file return; } let layoutTemplate = this.cacheByInputPath[layoutFilePath]; layoutTemplate.resetCaches(); let keys = layoutTemplate.getCacheKeys(); for (let key of keys) { delete this.cache[key]; } delete this.cacheByInputPath[layoutFilePath]; } } let layoutCache = new LayoutCache(); eventBus.on("eleventy.resourceModified", () => { // https://github.com/11ty/eleventy-plugin-bundle/issues/10 layoutCache.removeAll(); }); // singleton export default layoutCache; ================================================ FILE: src/Plugins/HtmlBasePlugin.js ================================================ import { DeepCopy } from "@11ty/eleventy-utils"; import urlFilter from "../Filters/Url.js"; import PathPrefixer from "../Util/PathPrefixer.js"; import { HtmlTransformer } from "../Util/HtmlTransformer.js"; import { isValidUrl } from "../Util/UrlUtil.js"; function addPathPrefixToUrl(url, pathPrefix, base) { let u; if (base) { u = new URL(url, base); } else { u = new URL(url); } // Add pathPrefix **after** url is transformed using base if (pathPrefix) { u.pathname = PathPrefixer.joinUrlParts(pathPrefix, u.pathname); } return u.toString(); } // pathprefix is only used when overrideBase is a full URL function transformUrl(url, base, opts = {}) { let { pathPrefix, pageUrl, htmlContext } = opts; // Warning, this will not work with HtmlTransformer, as we’ll receive "false" (string) here instead of `false` (boolean) if (url === false) { throw new Error( `Invalid url transformed in the HTML \`\` plugin.${url === false ? ` Did you attempt to link to a \`permalink: false\` page?` : ""} Received: ${url}`, ); } // full URL, return as-is if (isValidUrl(url)) { return url; } // Not a full URL, but with a full base URL // e.g. relative urls like "subdir/", "../subdir", "./subdir" if (isValidUrl(base)) { // convert relative paths to absolute path first using pageUrl if (pageUrl && !url.startsWith("/")) { let urlObj = new URL(url, `http://example.com${pageUrl}`); url = urlObj.pathname + (urlObj.hash || ""); } return addPathPrefixToUrl(url, pathPrefix, base); } // Not a full URL, nor a full base URL (call the built-in `url` filter) return urlFilter(url, base); } function HtmlBasePlugin(eleventyConfig, defaultOptions = {}) { let opts = DeepCopy( { // eleventyConfig.pathPrefix is new in Eleventy 2.0.0-canary.15 // `base` can be a directory (for path prefix transformations) // OR a full URL with origin and pathname baseHref: eleventyConfig.pathPrefix, extensions: "html", }, defaultOptions, ); // `filters` option to rename filters was removed in 3.0.0-alpha.13 // Renaming these would cause issues in other plugins (e.g. RSS) if (opts.filters !== undefined) { throw new Error( "The `filters` option in the HTML Base plugin was removed to prevent future cross-plugin compatibility issues.", ); } if (opts.baseHref === undefined) { throw new Error("The `baseHref` option is required in the HTML Base plugin."); } eleventyConfig.addFilter("addPathPrefixToFullUrl", function (url) { return addPathPrefixToUrl(url, eleventyConfig.pathPrefix); }); // Apply to one URL eleventyConfig.addFilter( "htmlBaseUrl", /** @this {object} */ function (url, baseOverride, pageUrlOverride) { let base = baseOverride || opts.baseHref; // Do nothing with a default base if (base === "/") { return url; } return transformUrl(url, base, { pathPrefix: eleventyConfig.pathPrefix, pageUrl: pageUrlOverride || this.page?.url, }); }, ); // Apply to a block of HTML eleventyConfig.addAsyncFilter( "transformWithHtmlBase", /** @this {object} */ function (content, baseOverride, pageUrlOverride) { let base = baseOverride || opts.baseHref; // Do nothing with a default base if (base === "/") { return content; } return HtmlTransformer.transformStandalone(content, (url, htmlContext) => { return transformUrl(url.trim(), base, { pathPrefix: eleventyConfig.pathPrefix, pageUrl: pageUrlOverride || this.page?.url, htmlContext, }); }); }, ); // Apply to all HTML output in your project eleventyConfig.htmlTransformer.addUrlTransform( opts.extensions, /** @this {object} */ function (urlInMarkup, htmlContext) { // baseHref override is via renderTransforms filter for adding the absolute URL (e.g. https://example.com/pathPrefix/) for RSS/Atom/JSON feeds return transformUrl(urlInMarkup.trim(), this.baseHref || opts.baseHref, { pathPrefix: eleventyConfig.pathPrefix, pageUrl: this.url, htmlContext, }); }, { priority: -2, // priority is descending, so this runs last (especially after AutoCopy and InputPathToUrl transform) enabled: function (context) { // Enabled when pathPrefix is non-default or via renderTransforms return Boolean(context.baseHref) || opts.baseHref !== "/"; }, }, ); } Object.defineProperty(HtmlBasePlugin, "eleventyPackage", { value: "@11ty/eleventy/html-base-plugin", }); Object.defineProperty(HtmlBasePlugin, "eleventyPluginOptions", { value: { unique: true, }, }); // CommonJS friendly exports on .default Object.assign(HtmlBasePlugin, { applyBaseToUrl: transformUrl, }); export default HtmlBasePlugin; export { transformUrl as applyBaseToUrl }; ================================================ FILE: src/Plugins/HtmlRelativeCopyPlugin.js ================================================ import { HtmlRelativeCopy } from "../Util/HtmlRelativeCopy.js"; // https://github.com/11ty/eleventy/pull/3573 // one HtmlRelativeCopy instance per entry function init(eleventyConfig, options) { if (!eleventyConfig.htmlTransformer) { throw new Error( "html-relative Passthrough Copy requires eleventyConfig.htmlTransformer support. Are you using the `@11ty/client` bundle? If so, try the `@11ty/client/eleventy` bundle instead.", ); } let opts = Object.assign( { extensions: "html", match: false, // can be one glob string or an array of globs paths: [], // directories to also look in for files failOnError: true, // fails when a path matches (via `match`) but not found on file system copyOptions: undefined, }, options, ); let htmlrel = new HtmlRelativeCopy(); htmlrel.setUserConfig(eleventyConfig); htmlrel.addMatchingGlob(opts.match); htmlrel.setFailOnError(opts.failOnError); htmlrel.setCopyOptions(opts.copyOptions); eleventyConfig.htmlTransformer.addUrlTransform( opts.extensions, function (targetFilepathOrUrl) { // @ts-ignore htmlrel.copy(targetFilepathOrUrl, this.page.inputPath, this.page.outputPath); // TODO front matter option for manual copy return targetFilepathOrUrl; }, { enabled: () => htmlrel.isEnabled(), // - MUST run after other plugins but BEFORE HtmlBase plugin priority: -1, }, ); htmlrel.addPaths(opts.paths); } function HtmlRelativeCopyPlugin(eleventyConfig) { // Important: if this is empty, no URL transforms are added for (let options of eleventyConfig.passthroughCopiesHtmlRelative) { init(eleventyConfig, options); } } Object.defineProperty(HtmlRelativeCopyPlugin, "eleventyPackage", { value: "@11ty/eleventy/html-relative-copy-plugin", }); export { HtmlRelativeCopyPlugin }; ================================================ FILE: src/Plugins/I18nPlugin.js ================================================ import { bcp47Normalize } from "bcp-47-normalize"; import iso639 from "iso-639-1"; import { DeepCopy } from "@11ty/eleventy-utils"; // pathPrefix note: // When using `locale_url` filter with the `url` filter, `locale_url` must run first like // `| locale_url | url`. If you run `| url | locale_url` it won’t match correctly. // TODO improvement would be to throw an error if `locale_url` finds a url with the // path prefix at the beginning? Would need a better way to know `url` has transformed a string // rather than just raw comparison. // e.g. --pathprefix=/en/ should return `/en/en/` for `/en/index.liquid` class LangUtils { static getLanguageCodeFromInputPath(filepath) { return (filepath || "").split("/").find((entry) => Comparator.isLangCode(entry)); } static getLanguageCodeFromUrl(url) { let s = (url || "").split("/"); return s.length > 0 && Comparator.isLangCode(s[1]) ? s[1] : ""; } static swapLanguageCodeNoCheck(str, langCode) { let found = false; return str .split("/") .map((entry) => { // only match the first one if (!found && Comparator.isLangCode(entry)) { found = true; return langCode; } return entry; }) .join("/"); } static swapLanguageCode(str, langCode) { if (!Comparator.isLangCode(langCode)) { return str; } return LangUtils.swapLanguageCodeNoCheck(str, langCode); } } class Comparator { // https://en.wikipedia.org/wiki/IETF_language_tag#Relation_to_other_standards // Requires a ISO-639-1 language code at the start (2 characters before the first -) static isLangCode(code) { let [s] = (code || "").split("-"); if (!iso639.validate(s)) { return false; } if (!bcp47Normalize(code)) { return false; } return true; } static urlHasLangCode(url, code) { if (!Comparator.isLangCode(code)) { return false; } return url.split("/").some((entry) => entry === code); } } function normalizeInputPath(inputPath, extensionMap) { if (extensionMap) { return extensionMap.removeTemplateExtension(inputPath); } return inputPath; } /* * Input: { * '/en-us/test/': './test/stubs-i18n/en-us/test.11ty.js', * '/en/test/': './test/stubs-i18n/en/test.liquid', * '/es/test/': './test/stubs-i18n/es/test.njk', * '/non-lang-file/': './test/stubs-i18n/non-lang-file.njk' * } * * Output: { * '/en-us/test/': [ { url: '/en/test/' }, { url: '/es/test/' } ], * '/en/test/': [ { url: '/en-us/test/' }, { url: '/es/test/' } ], * '/es/test/': [ { url: '/en-us/test/' }, { url: '/en/test/' } ] * } */ function getLocaleUrlsMap(urlToInputPath, extensionMap, options = {}) { let filemap = {}; for (let url in urlToInputPath) { // Group number comes from Pagination.js let { inputPath: originalFilepath, groupNumber } = urlToInputPath[url]; let filepath = normalizeInputPath(originalFilepath, extensionMap); let replaced = LangUtils.swapLanguageCodeNoCheck(filepath, "__11ty_i18n") + `_group:${groupNumber}`; if (!filemap[replaced]) { filemap[replaced] = []; } let langCode = LangUtils.getLanguageCodeFromInputPath(originalFilepath); if (!langCode) { langCode = LangUtils.getLanguageCodeFromUrl(url); } if (!langCode) { langCode = options.defaultLanguage; } if (langCode) { filemap[replaced].push({ url, lang: langCode, label: iso639.getNativeName(langCode.split("-")[0]), }); } else { filemap[replaced].push({ url }); } } // Default sorted by lang code for (let key in filemap) { filemap[key].sort(function (a, b) { if (a.lang < b.lang) { return -1; } if (a.lang > b.lang) { return 1; } return 0; }); } // map of input paths => array of localized urls let urlMap = {}; for (let filepath in filemap) { for (let entry of filemap[filepath]) { let url = entry.url; if (!urlMap[url]) { urlMap[url] = filemap[filepath].filter((entry) => { if (entry.lang) { return true; } return entry.url !== url; }); } } } return urlMap; } function I18nPlugin(eleventyConfig, opts = {}) { let options = DeepCopy( { defaultLanguage: "", filters: { url: "locale_url", links: "locale_links", }, errorMode: "strict", // allow-fallback, never }, opts, ); if (!options.defaultLanguage) { throw new Error( "You must specify a `defaultLanguage` in Eleventy’s Internationalization (I18N) plugin.", ); } let extensionMap; eleventyConfig.on("eleventy.extensionmap", (map) => { extensionMap = map; }); let bench = eleventyConfig.benchmarkManager.get("Aggregate"); let contentMaps = {}; eleventyConfig.on("eleventy.contentMap", function ({ urlToInputPath, inputPathToUrl }) { let b = bench.get("(i18n Plugin) Setting up content map."); b.before(); contentMaps.inputPathToUrl = inputPathToUrl; contentMaps.urlToInputPath = urlToInputPath; contentMaps.localeUrlsMap = getLocaleUrlsMap(urlToInputPath, extensionMap, options); b.after(); }); eleventyConfig.addGlobalData("eleventyComputed.page.lang", () => { // if addGlobalData receives a function it will execute it immediately, // so we return a nested function for computed data return (data) => { return LangUtils.getLanguageCodeFromUrl(data.page.url) || options.defaultLanguage; }; }); // Normalize a theoretical URL based on the current page’s language // If a non-localized file exists, returns the URL without a language assigned // Fails if no file exists (localized and not localized) eleventyConfig.addFilter(options.filters.url, function (url, langCodeOverride) { let langCode = langCodeOverride || LangUtils.getLanguageCodeFromUrl(this.page?.url) || options.defaultLanguage; // Already has a language code on it and has a relevant url with the target language code if ( contentMaps.localeUrlsMap[url] || (!url.endsWith("/") && contentMaps.localeUrlsMap[`${url}/`]) ) { for (let existingUrlObj of contentMaps.localeUrlsMap[url] || contentMaps.localeUrlsMap[`${url}/`]) { if (Comparator.urlHasLangCode(existingUrlObj.url, langCode)) { return existingUrlObj.url; } } } // Needs the language code prepended to the URL let prependedLangCodeUrl = `/${langCode}${url}`; if ( contentMaps.localeUrlsMap[prependedLangCodeUrl] || (!prependedLangCodeUrl.endsWith("/") && contentMaps.localeUrlsMap[`${prependedLangCodeUrl}/`]) ) { return prependedLangCodeUrl; } if ( contentMaps.urlToInputPath[url] || (!url.endsWith("/") && contentMaps.urlToInputPath[`${url}/`]) ) { // this is not a localized file (independent of a language code) if (options.errorMode === "strict") { throw new Error( `Localized file for URL ${prependedLangCodeUrl} was not found in your project. A non-localized version does exist—are you sure you meant to use the \`${options.filters.url}\` filter for this? You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`, ); } } else if (options.errorMode === "allow-fallback") { // You’re linking to a localized file that doesn’t exist! throw new Error( `Localized file for URL ${prependedLangCodeUrl} was not found in your project! You will need to add it if you want to link to it using the \`${options.filters.url}\` filter. You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`, ); } return url; }); // Refactor to use url // Find the links that are localized alternates to the inputPath argument eleventyConfig.addFilter(options.filters.links, function (urlOverride) { let url = urlOverride || this.page?.url; return (contentMaps.localeUrlsMap[url] || []).filter((entry) => { return entry.url !== url; }); }); // Returns a `page`-esque variable for the root default language page // If paginated, returns first result only eleventyConfig.addFilter( "locale_page", // This is not exposed in `options` because it is an Eleventy internals filter (used in get*CollectionItem filters) function (pageOverride, languageCode) { // both args here are optional if (!languageCode) { languageCode = options.defaultLanguage; } let page = pageOverride || this.page; let url; // new url if (contentMaps.localeUrlsMap[page.url]) { for (let entry of contentMaps.localeUrlsMap[page.url]) { if (entry.lang === languageCode) { url = entry.url; } } } let inputPath = LangUtils.swapLanguageCode(page.inputPath, languageCode); if ( !url || !Array.isArray(contentMaps.inputPathToUrl[inputPath]) || contentMaps.inputPathToUrl[inputPath].length === 0 ) { // no internationalized pages found return page; } let result = { // // note that the permalink/slug may be different for the localized file! url, inputPath, filePathStem: LangUtils.swapLanguageCode(page.filePathStem, languageCode), // outputPath is omitted here, not necessary for GetCollectionItem.js if url is provided __locale_page_resolved: true, }; return result; }, ); } export { Comparator, LangUtils }; Object.defineProperty(I18nPlugin, "eleventyPackage", { value: "@11ty/eleventy/i18n-plugin", }); Object.defineProperty(I18nPlugin, "eleventyPluginOptions", { value: { unique: true, }, }); // CommonJS friendly exports on .default Object.assign(I18nPlugin, { Comparator, LangUtils, }); export default I18nPlugin; ================================================ FILE: src/Plugins/IdAttributePlugin.js ================================================ import matchHelper from "posthtml-match-helper"; import { decodeHTML } from "entities"; const POSTHTML_PLUGIN_NAME = "11ty/eleventy/id-attribute"; function getTextNodeContent(node) { let ignoredAttr = node.attrs?.["eleventy:id-ignore"]; if (ignoredAttr === "" || ignoredAttr === true) { delete node.attrs["eleventy:id-ignore"]; return ""; } if (!node.content) { return ""; } return node.content .map((entry) => { if (typeof entry === "string") { return entry; } if (Array.isArray(entry.content)) { return getTextNodeContent(entry); } return ""; }) .join(""); } export function IdAttributePlugin(eleventyConfig, options = {}) { if (!options.slugify) { options.slugify = eleventyConfig.getFilter("slugify"); } if (!options.selector) { options.selector = "[id],h1,h2,h3,h4,h5,h6"; } options.decodeEntities = options.decodeEntities ?? true; options.checkDuplicates = options.checkDuplicates ?? "error"; eleventyConfig.htmlTransformer.addPosthtmlPlugin( "html", function idAttributePosthtmlPlugin(pluginOptions = {}) { if (typeof options.filter === "function") { if (options.filter(pluginOptions) === false) { return function () {}; } } return function (tree) { // One per page let conflictCheck = {}; // Cache heading nodes for conflict resolution let headingNodes = {}; tree.match(matchHelper(options.selector), function (node) { if (node.attrs?.id) { let id = node.attrs?.id; if (conflictCheck[id]) { conflictCheck[id]++; if (headingNodes[id]) { // Rename conflicting assigned heading id let newId = `${id}-${conflictCheck[id]}`; headingNodes[newId] = headingNodes[id]; headingNodes[newId].attrs.id = newId; delete headingNodes[id]; } else if (options.checkDuplicates === "error") { // Existing `id` conflicts with assigned heading id, throw error throw new Error( 'You have more than one HTML `id` attribute using the same value (id="' + id + '") in your template (' + pluginOptions.page.inputPath + "). You can disable this error in the IdAttribute plugin with the `checkDuplicates: false` option.", ); } } else { conflictCheck[id] = 1; } } else if (!node.attrs?.id && node.content) { node.attrs = node.attrs || {}; let textContent = getTextNodeContent(node); if (options.decodeEntities) { textContent = decodeHTML(textContent); } let id = options.slugify(textContent); if (conflictCheck[id]) { conflictCheck[id]++; id = `${id}-${conflictCheck[id]}`; } else { conflictCheck[id] = 1; } headingNodes[id] = node; node.attrs.id = id; } return node; }); }; }, { // pluginOptions name: POSTHTML_PLUGIN_NAME, }, ); } ================================================ FILE: src/Plugins/InputPathToUrl.js ================================================ import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import { isValidUrl } from "../Util/UrlUtil.js"; function getValidPath(contentMap, testPath) { // if the path is coming from Markdown, it may be encoded let normalized = TemplatePath.addLeadingDotSlash(decodeURIComponent(testPath)); // it must exist in the content map to be valid if (contentMap[normalized]) { return normalized; } } function normalizeInputPath(targetInputPath, inputDir, sourceInputPath, contentMap) { // inputDir is optional at the beginning of the developer supplied-path // Input directory already on the input path if (TemplatePath.join(targetInputPath).startsWith(TemplatePath.join(inputDir))) { let absolutePath = getValidPath(contentMap, targetInputPath); if (absolutePath) { return absolutePath; } } // Relative to project input directory let relativeToInputDir = getValidPath(contentMap, TemplatePath.join(inputDir, targetInputPath)); if (relativeToInputDir) { return relativeToInputDir; } if (targetInputPath && !path.isAbsolute(targetInputPath)) { // Relative to source file’s input path let sourceInputDir = TemplatePath.getDirFromFilePath(sourceInputPath); let relativeToSourceFile = getValidPath( contentMap, TemplatePath.join(sourceInputDir, targetInputPath), ); if (relativeToSourceFile) { return relativeToSourceFile; } } // the transform may have sent in a URL so we just return it as-is return targetInputPath; } function parseFilePath(filepath) { if (filepath.startsWith("#") || filepath.startsWith("?")) { return [filepath, ""]; } try { /* u: URL { href: 'file:///tmpl.njk#anchor', origin: 'null', protocol: 'file:', username: '', password: '', host: '', hostname: '', port: '', pathname: '/tmpl.njk', search: '', searchParams: URLSearchParams {}, hash: '#anchor' } */ // Note that `node:url` -> pathToFileURL creates an absolute path, which we don’t want // URL(`file:#anchor`) gives back a pathname of `/` let u = new URL(`file:${filepath}`); filepath = filepath.replace(u.search, ""); // includes ? filepath = filepath.replace(u.hash, ""); // includes # return [ // search includes ?, hash includes # u.search + u.hash, filepath, ]; } catch (e) { return ["", filepath]; } } function FilterPlugin(eleventyConfig) { let contentMap; eleventyConfig.on("eleventy.contentMap", function ({ inputPathToUrl }) { contentMap = inputPathToUrl; }); eleventyConfig.addFilter("inputPathToUrl", function (targetFilePath) { if (!contentMap) { throw new Error("Internal error: contentMap not available for `inputPathToUrl` filter."); } if (isValidUrl(targetFilePath)) { return targetFilePath; } let inputDir = eleventyConfig.directories.input; let suffix; [suffix, targetFilePath] = parseFilePath(targetFilePath); if (targetFilePath) { targetFilePath = normalizeInputPath( targetFilePath, inputDir, // @ts-ignore this.page.inputPath, contentMap, ); } let urls = contentMap[targetFilePath]; if (!urls || urls.length === 0) { throw new Error( "`inputPathToUrl` filter could not find a matching target for " + targetFilePath, ); } return `${urls[0]}${suffix}`; }); } function TransformPlugin(eleventyConfig, defaultOptions = {}) { let opts = Object.assign( { extensions: "html", }, defaultOptions, ); let contentMap = null; eleventyConfig.on("eleventy.contentMap", function ({ inputPathToUrl }) { contentMap = inputPathToUrl; }); eleventyConfig.htmlTransformer.addUrlTransform(opts.extensions, function (targetFilepathOrUrl) { if (!contentMap) { throw new Error("Internal error: contentMap not available for the `pathToUrl` Transform."); } if (isValidUrl(targetFilepathOrUrl)) { return targetFilepathOrUrl; } let inputDir = eleventyConfig.directories.input; let suffix; [suffix, targetFilepathOrUrl] = parseFilePath(targetFilepathOrUrl); if (targetFilepathOrUrl) { targetFilepathOrUrl = normalizeInputPath( targetFilepathOrUrl, inputDir, // @ts-ignore this.page.inputPath, contentMap, ); } let urls = contentMap[targetFilepathOrUrl]; if (!targetFilepathOrUrl || !urls || urls.length === 0) { // fallback, transforms don’t error on missing paths (though the pathToUrl filter does) return `${targetFilepathOrUrl}${suffix}`; } return `${urls[0]}${suffix}`; }); } Object.defineProperty(FilterPlugin, "eleventyPackage", { value: "@11ty/eleventy/inputpath-to-url-filter-plugin", }); Object.defineProperty(FilterPlugin, "eleventyPluginOptions", { value: { unique: true, }, }); Object.defineProperty(TransformPlugin, "eleventyPackage", { value: "@11ty/eleventy/inputpath-to-url-transform-plugin", }); Object.defineProperty(TransformPlugin, "eleventyPluginOptions", { value: { unique: true, }, }); export default TransformPlugin; export { FilterPlugin, TransformPlugin }; ================================================ FILE: src/Plugins/Pagination.js ================================================ import { isPlainObject } from "@11ty/eleventy-utils"; import lodash from "@11ty/lodash-custom"; import { DeepCopy } from "@11ty/eleventy-utils"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import { ProxyWrap } from "../Util/Objects/ProxyWrap.js"; // import { DeepFreeze } from "../Util/Objects/DeepFreeze.js"; import TemplateData from "../Data/TemplateData.js"; const { set: lodashSet, get: lodashGet, chunk: lodashChunk } = lodash; class PaginationConfigError extends EleventyBaseError {} class PaginationError extends EleventyBaseError {} class Pagination { constructor(tmpl, data, config) { if (!config) { throw new PaginationConfigError("Expected `config` argument to Pagination class."); } this.config = config; this.setTemplate(tmpl); this.setData(data); } get inputPathForErrorMessages() { if (this.template) { return ` (${this.template.inputPath})`; } return ""; } static hasPagination(data) { return "pagination" in data; } hasPagination() { if (!this.data) { throw new Error( `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, ); } return Pagination.hasPagination(this.data); } circularReferenceCheck(data) { let key = data.pagination.data; let includedTags = TemplateData.getIncludedTagNames(data); for (let tag of includedTags) { if (`collections.${tag}` === key) { throw new PaginationError( `Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` collection and also supplies pages to that collection.`, ); } } } setData(data) { this.data = data || {}; this.target = []; if (!this.hasPagination()) { return; } if (!data.pagination) { throw new Error( `Misconfigured pagination data in template front matter${this.inputPathForErrorMessages} (YAML front matter precaution: did you use tabs and not spaces for indentation?).`, ); } else if (!("size" in data.pagination)) { throw new Error( `Missing pagination size in front matter data${this.inputPathForErrorMessages}`, ); } this.circularReferenceCheck(data); this.size = data.pagination.size; this.alias = data.pagination.alias; this.fullDataSet = this._get(this.data, this._getDataKey()); // this returns an array this.target = this._resolveItems(); this.chunkedItems = this.pagedItems; } setTemplate(tmpl) { this.template = tmpl; } _getDataKey() { return this.data.pagination.data; } shouldResolveDataToObjectValues() { if ("resolve" in this.data.pagination) { return this.data.pagination.resolve === "values"; } return false; } isFiltered(value) { if ("filter" in this.data.pagination) { let filtered = this.data.pagination.filter; if (Array.isArray(filtered)) { return filtered.indexOf(value) > -1; } return filtered === value; } return false; } _has(target, key) { let notFoundValue = "__NOT_FOUND_ERROR__"; let data = lodashGet(target, key, notFoundValue); return data !== notFoundValue; } _get(target, key) { let notFoundValue = "__NOT_FOUND_ERROR__"; let data = lodashGet(target, key, notFoundValue); if (data === notFoundValue) { throw new Error( `Could not find pagination data${this.inputPathForErrorMessages}, went looking for: ${key}`, ); } return data; } _resolveItems() { let keys; if (Array.isArray(this.fullDataSet)) { keys = this.fullDataSet; this.paginationTargetType = "array"; } else if (isPlainObject(this.fullDataSet)) { this.paginationTargetType = "object"; if (this.shouldResolveDataToObjectValues()) { keys = Object.values(this.fullDataSet); } else { keys = Object.keys(this.fullDataSet); } } else { throw new Error( `Unexpected data found in pagination target${this.inputPathForErrorMessages}: expected an Array or an Object.`, ); } // keys must be an array let result = keys.slice(); if (this.data.pagination.before && typeof this.data.pagination.before === "function") { // we don’t need to make a copy of this because we .slice() above to create a new copy let fns = {}; if (this.config) { fns = this.config.javascriptFunctions; } result = this.data.pagination.before.call(fns, result, this.data); } if (this.data.pagination.reverse === true) { result = result.reverse(); } if (this.data.pagination.filter) { result = result.filter((value) => !this.isFiltered(value)); } return result; } get pagedItems() { if (!this.data) { throw new Error( `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, ); } const chunks = lodashChunk(this.target, this.size); if (this.data.pagination?.generatePageOnEmptyData) { return chunks.length ? chunks : [[]]; } else { return chunks; } } getPageCount() { if (!this.hasPagination()) { return 0; } return this.chunkedItems.length; } getNormalizedItems(pageItems) { return this.size === 1 ? pageItems[0] : pageItems; } getOverrideDataPages(items, pageNumber) { return { // See Issue #345 for more examples page: { previous: pageNumber > 0 ? this.getNormalizedItems(items[pageNumber - 1]) : null, next: pageNumber < items.length - 1 ? this.getNormalizedItems(items[pageNumber + 1]) : null, first: items.length ? this.getNormalizedItems(items[0]) : null, last: items.length ? this.getNormalizedItems(items[items.length - 1]) : null, }, pageNumber, }; } getOverrideDataLinks(pageNumber, templateCount, links) { let obj = {}; // links are okay but hrefs are better obj.previousPageLink = pageNumber > 0 ? links[pageNumber - 1] : null; obj.previous = obj.previousPageLink; obj.nextPageLink = pageNumber < templateCount - 1 ? links[pageNumber + 1] : null; obj.next = obj.nextPageLink; obj.firstPageLink = links.length > 0 ? links[0] : null; obj.lastPageLink = links.length > 0 ? links[links.length - 1] : null; obj.links = links; // todo deprecated, consistency with collections and use links instead obj.pageLinks = links; return obj; } getOverrideDataHrefs(pageNumber, templateCount, hrefs) { let obj = {}; // hrefs are better than links obj.previousPageHref = pageNumber > 0 ? hrefs[pageNumber - 1] : null; obj.nextPageHref = pageNumber < templateCount - 1 ? hrefs[pageNumber + 1] : null; obj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null; obj.lastPageHref = hrefs.length > 0 ? hrefs[hrefs.length - 1] : null; obj.hrefs = hrefs; // better names obj.href = { previous: obj.previousPageHref, next: obj.nextPageHref, first: obj.firstPageHref, last: obj.lastPageHref, }; return obj; } async getPageTemplates() { if (!this.data) { throw new Error( `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, ); } if (!this.hasPagination()) { return []; } let entries = []; let items = this.chunkedItems; let pages = this.size === 1 ? items.map((entry) => entry[0]) : items; let links = []; let hrefs = []; let hasPermalinkField = Boolean(this.data[this.config.keys.permalink]) || Boolean(this.data.eleventyComputed?.[this.config.keys.permalink]); // Do *not* pass collections through DeepCopy, we’ll re-add them back in later. let collections = this.data.collections; if (collections) { delete this.data.collections; } let parentData = DeepCopy( { pagination: { data: this.data.pagination.data, size: this.data.pagination.size, alias: this.alias, pages, }, }, this.data, ); // Restore skipped collections if (collections) { this.data.collections = collections; // Keep the original reference to the collections, no deep copy!! parentData.collections = collections; } // TODO this does work fine but let’s wait on enabling it. // DeepFreeze(parentData, ["collections"]); // TODO future improvement dea: use a light Template wrapper for paged template clones (PagedTemplate?) // so that we don’t have the memory cost of the full template (and can reuse the parent // template for some things) let indices = new Set(); for (let j = 0; j <= items.length - 1; j++) { indices.add(j); } for (let pageNumber of indices) { let cloned = await this.template.clone(); if (pageNumber > 0 && !hasPermalinkField) { cloned.setExtraOutputSubdirectory(pageNumber); } let paginationData = { pagination: { items: items[pageNumber], }, page: {}, }; Object.assign(paginationData.pagination, this.getOverrideDataPages(items, pageNumber)); if (this.alias) { lodashSet(paginationData, this.alias, this.getNormalizedItems(items[pageNumber])); } // Do *not* deep merge pagination data! See https://github.com/11ty/eleventy/issues/147#issuecomment-440802454 let clonedData = ProxyWrap(paginationData, parentData); // Previous method: // let clonedData = DeepCopy(paginationData, parentData); let { /*linkInstance,*/ rawPath, path, href, dir } = await cloned.getOutputLocations(clonedData); // TODO subdirectory to links if the site doesn’t live at / if (rawPath) { links.push("/" + rawPath); } hrefs.push(href); // page.url and page.outputPath are used to avoid another getOutputLocations call later, see Template->addComputedData clonedData.page.url = href; clonedData.page.outputPath = path; clonedData.page.dir = dir; entries.push({ pageNumber, // This is used by i18n Plugin to allow subgroups of nested pagination to be separate groupNumber: items[pageNumber]?.[0]?.eleventyPaginationGroupNumber, template: cloned, data: clonedData, }); } // we loop twice to pass in the appropriate prev/next links (already full generated now) let index = 0; for (let pageEntry of entries) { let linksObj = this.getOverrideDataLinks(index, items.length, links); Object.assign(pageEntry.data.pagination, linksObj); let hrefsObj = this.getOverrideDataHrefs(index, items.length, hrefs); Object.assign(pageEntry.data.pagination, hrefsObj); index++; } return entries; } } export default Pagination; ================================================ FILE: src/Plugins/PreserveClosingTagsPlugin.js ================================================ const POSTHTML_PLUGIN_NAME = "11ty/eleventy/preserve-closing-tags"; export function PreserveClosingTagsPlugin(eleventyConfig, options = {}) { // TODO error on non-void eligible tag names if (!options.tags) { // "meta" options.tags = []; } if (!Array.isArray(options.tags)) { throw new Error("`tags` passed to the Preserve Closing Tags plugin must be an Array"); } const tagMatches = options.tags.map((tag) => ({ tag })); if (tagMatches.length === 0) { return; } eleventyConfig.htmlTransformer.addPosthtmlPlugin( "html", function preserveClosingTagsPlugin(pluginOptions = {}) { return function (tree) { tree.match(tagMatches, function (node) { node.closeAs = "slash"; // close eligible tags as `` and not `` return node; }); }; }, { // pluginOptions name: POSTHTML_PLUGIN_NAME, }, ); } ================================================ FILE: src/Plugins/RenderPlugin.js ================================================ import { readFileSync } from "node:fs"; import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; // TODO add a first-class Markdown component to expose this using Markdown-only syntax (will need to be synchronous for markdown-it) import { ProxyWrap } from "../Util/Objects/ProxyWrap.js"; import TemplateDataInitialGlobalData from "../Data/TemplateDataInitialGlobalData.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import TemplateRender from "../TemplateRender.js"; import ProjectDirectories from "../Util/ProjectDirectories.js"; import TemplateConfig from "../TemplateConfig.js"; import EleventyExtensionMap from "../EleventyExtensionMap.js"; import TemplateEngineManager from "../Engines/TemplateEngineManager.js"; import Liquid from "../Adapters/Engines/Liquid.js"; class EleventyNunjucksError extends EleventyBaseError {} /** @this {object} */ async function compile(content, templateLang, options = {}) { let { templateConfig, extensionMap } = options; let strictMode = options.strictMode ?? false; if (!templateConfig) { templateConfig = new TemplateConfig(null, false); templateConfig.setDirectories(new ProjectDirectories()); await templateConfig.init(); } // Breaking change in 2.0+, previous default was `html` and now we default to the page template syntax if (!templateLang) { templateLang = this.page.templateSyntax; } if (!extensionMap) { if (strictMode) { throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compile."); } extensionMap = new EleventyExtensionMap(templateConfig); extensionMap.engineManager = new TemplateEngineManager(templateConfig); } let tr = new TemplateRender(templateLang, templateConfig); tr.extensionMap = extensionMap; if (templateLang) { await tr.setEngineOverride(templateLang); } else { await tr.init(); } // TODO tie this to the class, not the extension if ( tr.engine.name === "11ty.js" || tr.engine.name === "11ty.cjs" || tr.engine.name === "11ty.mjs" || // TODO Node 22+ tr.engine.name === "11ty.ts" || tr.engine.name === "11ty.cts" || tr.engine.name === "11ty.mts" ) { throw new Error( "11ty.js is not yet supported as a template engine for `renderTemplate`. Use `renderFile` instead!", ); } return tr.getCompiledTemplate(content); } // No templateLang default, it should infer from the inputPath. async function compileFile(inputPath, options = {}, templateLang) { let { templateConfig, extensionMap, config } = options; let strictMode = options.strictMode ?? false; if (!inputPath) { throw new Error("Missing file path argument passed to the `renderFile` shortcode."); } let wasTemplateConfigMissing = false; if (!templateConfig) { templateConfig = new TemplateConfig(null, false); templateConfig.setDirectories(new ProjectDirectories()); wasTemplateConfigMissing = true; } if (config && typeof config === "function") { await config(templateConfig.userConfig); } if (wasTemplateConfigMissing) { await templateConfig.init(); } let normalizedPath = TemplatePath.normalizeOperatingSystemFilePath(inputPath); // Prefer the exists cache, if it’s available if (!templateConfig.existsCache.exists(normalizedPath)) { throw new Error( "Could not find render plugin file for the `renderFile` shortcode, looking for: " + inputPath, ); } if (!extensionMap) { if (strictMode) { throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compileFile."); } extensionMap = new EleventyExtensionMap(templateConfig); extensionMap.engineManager = new TemplateEngineManager(templateConfig); } let tr = new TemplateRender(inputPath, templateConfig); tr.extensionMap = extensionMap; if (templateLang) { await tr.setEngineOverride(templateLang); } else { await tr.init(); } if (!tr.engine.needsToReadFileContents()) { return tr.getCompiledTemplate(null); } // TODO we could make this work with full templates (with front matter?) let content = readFileSync(inputPath, "utf8"); return tr.getCompiledTemplate(content); } /** @this {object} */ async function renderShortcodeFn(fn, data) { if (fn === undefined) { return; } else if (typeof fn !== "function") { throw new Error(`The \`compile\` function did not return a function. Received ${fn}`); } // if the user passes a string or other literal, remap to an object. if (!isPlainObject(data)) { data = { _: data, }; } if ("data" in this && isPlainObject(this.data)) { // when options.accessGlobalData is true, this allows the global data // to be accessed inside of the shortcode as a fallback data = ProxyWrap(data, this.data); } else { // save `page` and `eleventy` for reuse data.page = this.page; data.eleventy = this.eleventy; } return fn(data); } /** * @module 11ty/eleventy/Plugins/RenderPlugin */ /** * A plugin to add shortcodes to render an Eleventy template * string (or file) inside of another template. {@link https://v3.11ty.dev/docs/plugins/render/} * * @since 1.0.0 * @param {module:11ty/eleventy/UserConfig} eleventyConfig - User-land configuration instance. * @param {object} options - Plugin options */ function RenderPlugin(eleventyConfig, options = {}) { let templateConfig; eleventyConfig.on("eleventy.config", (tmplConfigInstance) => { templateConfig = tmplConfigInstance; }); let extensionMap; eleventyConfig.on("eleventy.extensionmap", (map) => { extensionMap = map; }); /** * @typedef {object} options * @property {string} [tagName] - The shortcode name to render a template string. * @property {string} [tagNameFile] - The shortcode name to render a template file. * @property {module:11ty/eleventy/TemplateConfig} [templateConfig] - Configuration object * @property {boolean} [accessGlobalData] - Whether or not the template has access to the page’s data. */ options.tagName ??= "renderTemplate"; options.tagNameFile ??= "renderFile"; options.filterName ??= "renderContent"; options.templateConfig ??= null; options.accessGlobalData ??= false; function liquidTemplateTag(liquidEngine, tagName, extras) { const { evalToken } = extras; // via https://github.com/harttle/liquidjs/blob/b5a22fa0910c708fe7881ef170ed44d3594e18f3/src/builtin/tags/raw.ts return { parse: function (tagToken, remainTokens) { this.name = tagToken.name; if (eleventyConfig.liquid.parameterParsing === "builtin") { this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args); // note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class } else { this.legacyArgs = tagToken.args; } this.tokens = []; var stream = liquidEngine.parser .parseStream(remainTokens) .on("token", (token) => { if (token.name === "end" + tagName) stream.stop(); else this.tokens.push(token); }) .on("end", () => { throw new Error(`tag ${tagToken.getText()} not closed`); }); stream.start(); }, render: function* (ctx) { let normalizedContext = {}; if (ctx) { if (options.accessGlobalData) { // parent template data cascade normalizedContext.data = ctx.getAll(); } normalizedContext.page = ctx.get(["page"]); normalizedContext.eleventy = ctx.get(["eleventy"]); } let argArray = []; if (this.legacyArgs) { let rawArgs = Liquid.parseArguments(null, this.legacyArgs); for (let arg of rawArgs) { let b = yield liquidEngine.evalValue(arg, ctx); argArray.push(b); } } else if (this.orderedArgs) { for (let arg of this.orderedArgs) { let b = yield evalToken(arg, ctx); argArray.push(b); } } // plaintext paired shortcode content let body = this.tokens.map((token) => token.getText()).join(""); let ret = _renderStringShortcodeFn.call( normalizedContext, body, // templateLang, data ...argArray, ); yield ret; return ret; }, }; } // TODO I don’t think this works with whitespace control, e.g. {%- endrenderTemplate %} function nunjucksTemplateTag(NunjucksLib, tagName) { return new (function () { this.tags = [tagName]; this.parse = function (parser, nodes) { var tok = parser.nextToken(); var args = parser.parseSignature(true, true); const begun = parser.advanceAfterBlockEnd(tok.value); // This code was ripped from the Nunjucks parser for `raw` // https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/parser.js#L655 const endTagName = "end" + tagName; // Look for upcoming raw blocks (ignore all other kinds of blocks) const rawBlockRegex = new RegExp( "([\\s\\S]*?){%\\s*(" + tagName + "|" + endTagName + ")\\s*(?=%})%}", ); let rawLevel = 1; let str = ""; let matches; // Exit when there's nothing to match // or when we've found the matching "endraw" block while ((matches = parser.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) { const all = matches[0]; const pre = matches[1]; const blockName = matches[2]; // Adjust rawlevel if (blockName === tagName) { rawLevel += 1; } else if (blockName === endTagName) { rawLevel -= 1; } // Add to str if (rawLevel === 0) { // We want to exclude the last "endraw" str += pre; // Move tokenizer to beginning of endraw block parser.tokens.backN(all.length - pre.length); } else { str += all; } } let body = new nodes.Output(begun.lineno, begun.colno, [ new nodes.TemplateData(begun.lineno, begun.colno, str), ]); return new nodes.CallExtensionAsync(this, "run", args, [body]); }; this.run = function (...args) { let resolve = args.pop(); let body = args.pop(); let [context, ...argArray] = args; let normalizedContext = {}; if (context.ctx?.page) { normalizedContext.ctx = context.ctx; // TODO .data // if(options.accessGlobalData) { // normalizedContext.data = context.ctx; // } normalizedContext.page = context.ctx.page; normalizedContext.eleventy = context.ctx.eleventy; } body(function (e, bodyContent) { if (e) { resolve( new EleventyNunjucksError(`Error with Nunjucks paired shortcode \`${tagName}\``, e), ); } Promise.resolve( _renderStringShortcodeFn.call( normalizedContext, bodyContent, // templateLang, data ...argArray, ), ).then( function (returnValue) { resolve(null, new NunjucksLib.runtime.SafeString(returnValue)); }, function (e) { resolve( new EleventyNunjucksError(`Error with Nunjucks paired shortcode \`${tagName}\``, e), null, ); }, ); }); }; })(); } /** @this {object} */ async function _renderStringShortcodeFn(content, templateLang, data = {}) { // Default is fn(content, templateLang, data) but we want to support fn(content, data) too if (typeof templateLang !== "string") { data = templateLang; templateLang = false; } // TODO Render plugin `templateLang` is feeding bad input paths to the addDependencies call in Custom.js let fn = await compile.call(this, content, templateLang, { templateConfig: options.templateConfig || templateConfig, extensionMap, }); return renderShortcodeFn.call(this, fn, data); } /** @this {object} */ async function _renderFileShortcodeFn(inputPath, data = {}, templateLang) { let renderFileOptions = { templateConfig: options.templateConfig || templateConfig, extensionMap, }; let fn = await compileFile.call(this, inputPath, renderFileOptions, templateLang); return renderShortcodeFn.call(this, fn, data); } // Render strings if (options.tagName) { // use falsy to opt-out eleventyConfig.addJavaScriptFunction(options.tagName, _renderStringShortcodeFn); eleventyConfig.addLiquidTag(options.tagName, function (liquidEngine, extras) { return liquidTemplateTag(liquidEngine, options.tagName, extras); }); eleventyConfig.addNunjucksTag(options.tagName, function (nunjucksLib) { return nunjucksTemplateTag(nunjucksLib, options.tagName); }); } // Filter for rendering strings if (options.filterName) { eleventyConfig.addAsyncFilter(options.filterName, _renderStringShortcodeFn); } // Render File // use `false` to opt-out if (options.tagNameFile) { eleventyConfig.addAsyncShortcode(options.tagNameFile, _renderFileShortcodeFn); } } // Will re-use the same configuration instance both at a top level and across any nested renders class RenderManager { /** @type {Promise|undefined} */ #hasConfigInitialized; #extensionMap; #templateConfig; constructor() { this.templateConfig = new TemplateConfig(null, false); this.templateConfig.setDirectories(new ProjectDirectories()); } get templateConfig() { return this.#templateConfig; } set templateConfig(templateConfig) { if (!templateConfig || templateConfig === this.#templateConfig) { return; } this.#templateConfig = templateConfig; // This is the only plugin running on the Edge this.#templateConfig.userConfig.addPlugin(RenderPlugin, { templateConfig: this.#templateConfig, accessGlobalData: true, }); this.#extensionMap = new EleventyExtensionMap(this.#templateConfig); this.#extensionMap.engineManager = new TemplateEngineManager(this.#templateConfig); } async init() { if (this.#hasConfigInitialized) { return this.#hasConfigInitialized; } if (this.templateConfig.hasInitialized()) { return true; } this.#hasConfigInitialized = this.templateConfig.init(); await this.#hasConfigInitialized; return true; } // `callback` is async-friendly but requires await upstream config(callback) { // run an extra `function(eleventyConfig)` configuration callbacks if (callback && typeof callback === "function") { return callback(this.templateConfig.userConfig); } } get initialGlobalData() { if (!this._data) { this._data = new TemplateDataInitialGlobalData(this.templateConfig); } return this._data; } // because we don’t have access to the full data cascade—but // we still want configuration data added via `addGlobalData` async getData(...data) { await this.init(); let globalData = await this.initialGlobalData.getData(); let merged = Merge({}, globalData, ...data); return merged; } async compile(content, templateLang, options = {}) { await this.init(); options.templateConfig = this.templateConfig; options.extensionMap = this.#extensionMap; options.strictMode = true; // We don’t need `compile.call(this)` here because the Edge always uses "liquid" as the template lang (instead of relying on this.page.templateSyntax) // returns promise return compile(content, templateLang, options); } async render(fn, edgeData, buildTimeData) { await this.init(); let mergedData = await this.getData(edgeData); // Set .data for options.accessGlobalData feature let context = { data: mergedData, }; return renderShortcodeFn.call(context, fn, buildTimeData); } } Object.defineProperty(RenderPlugin, "eleventyPackage", { value: "@11ty/eleventy/render-plugin", }); Object.defineProperty(RenderPlugin, "eleventyPluginOptions", { value: { unique: true, }, }); // CommonJS friendly exports on .default Object.assign(RenderPlugin, { File: compileFile, String: compile, RenderManager, }); export default RenderPlugin; export { compileFile as File, compile as String, RenderManager }; ================================================ FILE: src/Template.js ================================================ import { parse } from "node:path"; import { statSync } from "node:fs"; import lodash from "@11ty/lodash-custom"; import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import chalk from "./Adapters/Packages/chalk.js"; import ConsoleLogger from "./Util/ConsoleLogger.js"; import { getCreatedTimestamp, getUpdatedTimestamp } from "./Util/Git.js"; import TemplateContent from "./TemplateContent.js"; import TemplatePermalink from "./TemplatePermalink.js"; import TemplateLayout from "./TemplateLayout.js"; import TemplateFileSlug from "./TemplateFileSlug.js"; import ComputedData from "./Data/ComputedData.js"; import Pagination from "./Plugins/Pagination.js"; import TemplateBehavior from "./TemplateBehavior.js"; import TemplateContentPrematureUseError from "./Errors/TemplateContentPrematureUseError.js"; import TemplateContentUnrenderedTemplateError from "./Errors/TemplateContentUnrenderedTemplateError.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import { fromISOtoDateUTC } from "./Util/DateParse.js"; import ReservedData from "./Util/ReservedData.js"; import TransformsUtil from "./Util/TransformsUtil.js"; import { FileSystemManager } from "./Util/FileSystemManager.js"; import { TemplatePreprocessors } from "./TemplatePreprocessors.js"; import PathNormalizer from "./Util/PathNormalizer.js"; import { getDirectoryFromUrl } from "./Util/UrlUtil.js"; const { set: lodashSet, get: lodashGet } = lodash; const debug = debugUtil("Eleventy:Template"); const debugDev = debugUtil("Dev:Eleventy:Template"); class Template extends TemplateContent { #logger; #fsManager; #stats; #preprocessorCache; constructor(templatePath, templateData, extensionMap, config) { debugDev("new Template(%o)", templatePath); super(templatePath, config); this.parsed = parse(templatePath); // for pagination this.extraOutputSubdirectory = ""; this.extensionMap = extensionMap; this.templateData = templateData; this.#initFileSlug(); this.linters = []; this.transforms = {}; this.isVerbose = true; this.isDryRun = false; this.writeCount = 0; this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig); this.fileSlugStr = this.fileSlug.getSlug(); this.filePathStem = this.fileSlug.getFullPathWithoutExtension(); this.outputFormat = "fs"; this.behavior = new TemplateBehavior(this.config); this.behavior.setOutputFormat(this.outputFormat); this.templatePreprocessor = new TemplatePreprocessors(this.config.preprocessors); } #initFileSlug() { this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig); this.fileSlugStr = this.fileSlug.getSlug(); this.filePathStem = this.fileSlug.getFullPathWithoutExtension(); } /* mimic constructor arg order */ resetCachedTemplate({ templateData, extensionMap, eleventyConfig }) { super.resetCachedTemplate({ eleventyConfig }); this.templateData = templateData; this.extensionMap = extensionMap; // this.#fsManager = undefined; this.#initFileSlug(); } get fsManager() { if (!this.#fsManager) { this.#fsManager = new FileSystemManager(this.eleventyConfig); } return this.#fsManager; } get logger() { if (!this.#logger) { this.#logger = new ConsoleLogger(); this.#logger.isVerbose = this.isVerbose; } return this.#logger; } /* Setter for Logger */ set logger(logger) { this.#logger = logger; } isRenderable() { return this.behavior.isRenderable(); } isRenderableDisabled() { return this.behavior.isRenderableDisabled(); } isRenderableOptional() { // A template that is lazily rendered once if used by a second order dependency of another template dependency. // e.g. You change firstpost.md, which is used by feed.xml, but secondpost.md (also used by feed.xml) // has not yet rendered and needs to be rendered once to populate the cache. return this.behavior.isRenderableOptional(); } setRenderableOverride(renderableOverride) { this.behavior.setRenderableOverride(renderableOverride); } reset() { this.renderCount = 0; this.writeCount = 0; } resetCaches(types) { types = this.getResetTypes(types); super.resetCaches(types); if (types.data || types.read) { this.#preprocessorCache = undefined; } if (types.data) { delete this._dataCache; // delete this._usePermalinkRoot; // delete this.#stats; } if (types.render) { delete this._cacheRenderedPromise; delete this._cacheRenderedTransformsAndLayoutsPromise; } } setOutputFormat(to) { this.outputFormat = to; this.behavior.setOutputFormat(to); } setIsVerbose(isVerbose) { this.isVerbose = isVerbose; this.logger.isVerbose = isVerbose; } setDryRunViaIncremental(isIncremental) { this.isDryRun = isIncremental; this.isIncremental = isIncremental; } setDryRun(isDryRun) { this.isDryRun = !!isDryRun; } setExtraOutputSubdirectory(dir) { this.extraOutputSubdirectory = dir + "/"; } getTemplateSubfolder() { let dir = TemplatePath.absolutePath(this.parsed.dir); let inputDir = TemplatePath.absolutePath(this.inputDir); // Browser virtual fs uses `/` root for absolute paths // Fixed in @11ty/eleventy-utils@2.0.8 or newer (can remove this later) if (inputDir === "/" && dir.startsWith("/")) { return dir; } return TemplatePath.stripLeadingSubPath(dir, inputDir); } templateUsesLayouts(pageData) { if (this.hasTemplateRender()) { return pageData?.[this.config.keys.layout] && this.templateRender.engine.useLayouts(); } // If `layout` prop is set, default to true when engine is unknown return Boolean(pageData?.[this.config.keys.layout]); } getLayout(layoutKey) { // already cached downstream in TemplateLayout -> TemplateCache try { return TemplateLayout.getTemplate(layoutKey, this.eleventyConfig, this.extensionMap); } catch (e) { throw new EleventyBaseError( `Problem creating an Eleventy Layout for the "${this.inputPath}" template file.`, e, ); } } get baseFile() { return this.extensionMap.removeTemplateExtension(this.parsed.base); } async _getLink(data) { if (!data) { throw new Error("Internal error: data argument missing in Template->_getLink"); } let permalink = data[this.config.keys.permalink] ?? data?.[this.config.keys.computed]?.[this.config.keys.permalink]; let permalinkValue; let isDynamicPermalinkEnabled = this.config.dynamicPermalinks && data.dynamicPermalink !== false; // `permalink: false` means render but no file system write, e.g. use in collections only) // `permalink: true` throws an error if (typeof permalink === "boolean") { debugDev("Using boolean permalink %o", permalink); permalinkValue = permalink; } else if (permalink && !isDynamicPermalinkEnabled) { // Issue #838 debugDev("Not using dynamic permalinks, using %o", permalink); permalinkValue = permalink; } else if (isPlainObject(permalink)) { // Empty permalink {} object should act as if no permalink was set at all // and inherit the default behavior let isEmptyObject = Object.keys(permalink).length === 0; if (!isEmptyObject) { let promises = []; let keys = []; for (let key in permalink) { keys.push(key); if (key !== "build" && Array.isArray(permalink[key])) { promises.push( Promise.all([...permalink[key]].map((entry) => super.renderPermalink(entry, data))), ); } else { promises.push(super.renderPermalink(permalink[key], data)); } } let results = await Promise.all(promises); permalinkValue = {}; for (let j = 0, k = keys.length; j < k; j++) { let key = keys[j]; permalinkValue[key] = results[j]; debug( "Rendering permalink.%o for %o: %s becomes %o", key, this.inputPath, permalink[key], results[j], ); } } } else if (permalink) { // render variables inside permalink front matter, bypass markdown permalinkValue = await super.renderPermalink(permalink, data); debug("Rendering permalink for %o: %s becomes %o", this.inputPath, permalink, permalinkValue); debugDev("Permalink rendered with data: %o", data); } // Override default permalink behavior. Only do this if permalink was _not_ in the data cascade if (!permalink && isDynamicPermalinkEnabled) { let tr = await this.getTemplateRender(); let permalinkCompilation = tr.engine.permalinkNeedsCompilation(""); if (typeof permalinkCompilation === "function") { let ret = await this._renderFunction(permalinkCompilation, permalinkValue, this.inputPath); if (ret !== undefined) { if (typeof ret === "function") { // function permalinkValue = await this._renderFunction(ret, data); } else { // scalar permalinkValue = ret; } } } } if (permalinkValue !== undefined) { let p = new TemplatePermalink( permalinkValue, this.extraOutputSubdirectory, isDynamicPermalinkEnabled, ); p.setUrlTransforms(this.config.urlTransforms); this.behavior.setFromPermalink(p); return p; } // No `permalink` specified in data cascade, do the default let p = TemplatePermalink.generate( this.getTemplateSubfolder(), this.baseFile, this.extraOutputSubdirectory, this.engine.defaultTemplateFileExtension, isDynamicPermalinkEnabled, ); p.setUrlTransforms(this.config.urlTransforms); return p; } async usePermalinkRoot() { // @cachedproperty if (this._usePermalinkRoot === undefined) { // TODO this only works with immediate front matter and not data files let { data } = await this.getFrontMatterData(); this._usePermalinkRoot = data[this.config.keys.permalinkRoot]; } return this._usePermalinkRoot; } async getOutputLocations(data) { this.bench.get("(count) getOutputLocations").incrementCount(); let link = await this._getLink(data); let path; if (await this.usePermalinkRoot()) { path = link.toPathFromRoot(); } else { path = link.toPath(this.outputDir); } let href = link.toHref(); return { linkInstance: link, rawPath: link.toOutputPath(), // includes output directory href, path: path, dir: getDirectoryFromUrl(href), // for `page.dir` }; } // This is likely now a test-only method // Preferred to use the singular `getOutputLocations` above. async getRawOutputPath(data) { this.bench.get("(count) getRawOutputPath").incrementCount(); let link = await this._getLink(data); return link.toOutputPath(); } // Preferred to use the singular `getOutputLocations` above. async getOutputHref(data) { this.bench.get("(count) getOutputHref").incrementCount(); let link = await this._getLink(data); return link.toHref(); } // Preferred to use the singular `getOutputLocations` above. async getOutputPath(data) { this.bench.get("(count) getOutputPath").incrementCount(); let link = await this._getLink(data); if (await this.usePermalinkRoot()) { return link.toPathFromRoot(); } return link.toPath(this.outputDir); } async _testGetAllLayoutFrontMatterData() { let { data: frontMatterData } = await this.getFrontMatterData(); if (frontMatterData[this.config.keys.layout]) { let layout = this.getLayout(frontMatterData[this.config.keys.layout]); return await layout.getData(); } return {}; } async #getData() { debugDev("%o getData", this.inputPath); let localData = {}; let globalData = {}; if (this.templateData) { localData = await this.templateData.getTemplateDirectoryData(this.inputPath); globalData = await this.templateData.getGlobalData(); debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath); } let { data: frontMatterData } = await this.getFrontMatterData(); let mergedLayoutData = {}; let tr = await this.getTemplateRender(); if (tr.engine.useLayouts()) { let layoutKey = frontMatterData[this.config.keys.layout] || localData[this.config.keys.layout] || globalData[this.config.keys.layout]; // Layout front matter data if (layoutKey) { let layout = this.getLayout(layoutKey); mergedLayoutData = await layout.getData(); debugDev("%o getData merged layout chain front matter", this.inputPath); } } try { let mergedData = Merge({}, globalData, mergedLayoutData, localData, frontMatterData); if (this.config.freezeReservedData) { ReservedData.checkSubset(mergedData); } await this.addPage(mergedData); debugDev("%o getData mergedData", this.inputPath); return mergedData; } catch (e) { // if ReservedDataError, defer to that (from ReservedData.checkSubset above) if (!ReservedData.isReservedDataError(e) && ReservedData.isFrozenError(e)) { throw ReservedData.getError({ cause: e }); } throw e; } } async getData() { if (!this._dataCache) { // @cachedproperty this._dataCache = this.#getData(); } return this._dataCache; } async addPage(data) { if (!("page" in data)) { data.page = {}; } // Make sure to keep these keys synchronized in src/Util/ReservedData.js data.page.inputPath = this.inputPath; // parsed dir never has the trailing slash data.page.inputPathDir = PathNormalizer.getDirectoryFromFilePath(this.inputPath); data.page.fileSlug = this.fileSlugStr; data.page.filePathStem = this.filePathStem; data.page.outputFileExtension = this.engine.defaultTemplateFileExtension; data.page.templateSyntax = this.getEngineNames(data[this.config.keys.engineOverride]); let newDate = await this.getMappedDate(data); // Skip date assignment if custom date is falsy. if (newDate) { data.page.date = newDate; } // data.page.url // data.page.outputPath // data.page.excerpt from gray-matter and Front Matter // data.page.lang from I18nPlugin } // Tests only async render() { throw new Error("Internal error: `Template->render` was removed in Eleventy 3.0."); } // Tests only async renderLayout() { throw new Error("Internal error: `Template->renderLayout` was removed in Eleventy 3.0."); } async renderDirect(str, data, bypassMarkdown) { return super.render(str, data, bypassMarkdown); } // This is the primary render mechanism, called via TemplateMap->populateContentDataInMap async renderPageEntryWithoutLayout(pageEntry) { // @cachedproperty if (!this._cacheRenderedPromise) { this._cacheRenderedPromise = this.renderDirect(pageEntry.rawInput, pageEntry.data); this.renderCount++; } return this._cacheRenderedPromise; } setLinters(linters) { if (!isPlainObject(linters)) { throw new Error("Object expected in setLinters"); } // this acts as a reset this.linters = []; for (let linter of Object.values(linters).filter((l) => typeof l === "function")) { this.addLinter(linter); } } addLinter(callback) { this.linters.push(callback); } async runLinters(str, page) { let { inputPath, outputPath, url } = page; let pageData = page.data.page; for (let linter of this.linters) { // these can be asynchronous but no guarantee of order when they run linter.call( { inputPath, outputPath, url, page: pageData, }, str, inputPath, outputPath, ); } } setTransforms(transforms) { if (!isPlainObject(transforms)) { throw new Error("Object expected in setTransforms"); } this.transforms = transforms; } async runTransforms(str, pageEntry) { return TransformsUtil.runAll(str, pageEntry.data.page, this.transforms, { logger: this.logger, }); } async #renderComputedUnit(entry, data) { if (typeof entry === "string") { return this.renderComputedData(entry, data); } if (isPlainObject(entry)) { for (let key in entry) { entry[key] = await this.#renderComputedUnit(entry[key], data); } } if (Array.isArray(entry)) { for (let j = 0, k = entry.length; j < k; j++) { entry[j] = await this.#renderComputedUnit(entry[j], data); } } return entry; } _addComputedEntry(computedData, obj, parentKey, declaredDependencies) { // this check must come before isPlainObject if (typeof obj === "function") { computedData.add(parentKey, obj, declaredDependencies); } else if (Array.isArray(obj) || typeof obj === "string") { // Arrays are treated as one entry in the dependency graph now, Issue #3728 computedData.addTemplateString( parentKey, async function (innerData) { return this.tmpl.#renderComputedUnit(obj, innerData); }, declaredDependencies, this.getParseForSymbolsFunction(obj), this, ); } else if (isPlainObject(obj)) { // Arrays used to be computed here for (let key in obj) { let keys = []; if (parentKey) { keys.push(parentKey); } keys.push(key); this._addComputedEntry(computedData, obj[key], keys.join("."), declaredDependencies); } } else { // Numbers, booleans, etc computedData.add(parentKey, obj, declaredDependencies); } } async addComputedData(data) { if (isPlainObject(data?.[this.config.keys.computed])) { this.computedData = new ComputedData(this.config); // Note that `permalink` is only a thing that gets consumed—it does not go directly into generated data // this allows computed entries to use page.url or page.outputPath and they’ll be resolved properly // TODO Room for optimization here—we don’t need to recalculate `getOutputHref` and `getOutputPath` // TODO Why are these using addTemplateString instead of add this.computedData.addTemplateString( "page.url", async function (data) { return this.tmpl.getOutputHref(data); }, data.permalink ? ["permalink"] : undefined, false, // skip symbol resolution this, ); this.computedData.addTemplateString( "page.outputPath", async function (data) { return this.tmpl.getOutputPath(data); }, data.permalink ? ["permalink"] : undefined, false, // skip symbol resolution this, ); // Check for reserved properties in computed data if (this.config.freezeReservedData) { ReservedData.checkSubset(data[this.config.keys.computed]); } // actually add the computed data this._addComputedEntry(this.computedData, data[this.config.keys.computed]); // limited run of computed data—save the stuff that relies on collections for later. debug("First round of computed data for %o", this.inputPath); await this.computedData.setupData(data, function (entry) { return !this.isUsesStartsWith(entry, "collections."); // TODO possible improvement here is to only process page.url, page.outputPath, permalink // instead of only punting on things that rely on collections. // let firstPhaseComputedData = ["page.url", "page.outputPath", ...this.getOrderFor("page.url"), ...this.getOrderFor("page.outputPath")]; // return firstPhaseComputedData.indexOf(entry) > -1; }); } else { if (!("page" in data)) { data.page = {}; } // pagination will already have these set via Pagination->getPageTemplates if (data.page.url && data.page.outputPath) { return; } let { href, path, dir } = await this.getOutputLocations(data); data.page.url = href; data.page.outputPath = path; data.page.dir = dir; } } // Computed data consuming collections! async resolveRemainingComputedData(data) { // If it doesn’t exist, computed data is not used for this template if (this.computedData) { debug("Second round of computed data for %o", this.inputPath); return this.computedData.processRemainingData(data); } } static augmentWithTemplateContentProperty(obj) { return Object.defineProperties(obj, { needsCheck: { enumerable: false, writable: true, value: true, }, _templateContent: { enumerable: false, writable: true, value: undefined, }, templateContent: { enumerable: true, set(content) { if (content === undefined) { this.needsCheck = false; } this._templateContent = content; }, get() { if (this.needsCheck && this._templateContent === undefined) { if (this.template.isRenderable()) { // should at least warn here throw new TemplateContentPrematureUseError( `Tried to use templateContent too early on ${this.inputPath}${ this.pageNumber ? ` (page ${this.pageNumber})` : "" }`, ); } else { throw new TemplateContentUnrenderedTemplateError( `Tried to use templateContent on unrendered template: ${ this.inputPath }${this.pageNumber ? ` (page ${this.pageNumber})` : ""}`, ); } } return this._templateContent; }, }, // Alias for templateContent for consistency content: { enumerable: true, get() { return this.templateContent; }, set() { throw new Error("Setter not available for `content`. Use `templateContent` instead."); }, }, }); } async runPreprocessors(data) { // @cachedproperty if (!this.#preprocessorCache) { this.#preprocessorCache = this.templatePreprocessor.runAll(this, data); } return this.#preprocessorCache; } async getTemplates(data) { let { skippedVia: skippedViaPreprocessorName, content: rawInput } = await this.runPreprocessors(data); if (skippedViaPreprocessorName) { debug( "Skipping %o, the %o preprocessor returned an explicit `false`", this.inputPath, skippedViaPreprocessorName, ); return []; } // Raw Input *includes* preprocessor modifications // https://github.com/11ty/eleventy/issues/1206 data.page.rawInput = rawInput; if (!Pagination.hasPagination(data)) { await this.addComputedData(data); let obj = { template: this, // not on the docs but folks are relying on it rawInput, groupNumber: 0, // i18n plugin data, page: data.page, inputPath: this.inputPath, fileSlug: this.fileSlugStr, filePathStem: this.filePathStem, date: data.page.date, outputPath: data.page.outputPath, url: data.page.url, }; obj = Template.augmentWithTemplateContentProperty(obj); return [obj]; } else { // needs collections for pagination items // but individual pagination entries won’t be part of a collection this.paging = new Pagination(this, data, this.config); let pageTemplates = await this.paging.getPageTemplates(); let objects = []; for (let pageEntry of pageTemplates) { await pageEntry.template.addComputedData(pageEntry.data); let obj = { template: pageEntry.template, // not on the docs but folks are relying on it rawInput, pageNumber: pageEntry.pageNumber, groupNumber: pageEntry.groupNumber || 0, data: pageEntry.data, inputPath: this.inputPath, fileSlug: this.fileSlugStr, filePathStem: this.filePathStem, page: pageEntry.data.page, date: pageEntry.data.page.date, outputPath: pageEntry.data.page.outputPath, url: pageEntry.data.page.url, }; obj = Template.augmentWithTemplateContentProperty(obj); objects.push(obj); } return objects; } } async _write({ url, outputPath, data, rawInput }, finalContent) { let lang = { start: "Writing", finished: "written", }; if (!this.isDryRun) { if (this.logger.isLoggingEnabled()) { let isVirtual = this.isVirtualTemplate(); let tr = await this.getTemplateRender(); let engineList = tr.getReadableEnginesListDifferingFromFileExtension(); let suffix = `${isVirtual ? " (virtual)" : ""}${engineList ? ` (${engineList})` : ""}`; this.logger.log( `${lang.start} ${outputPath} ${chalk.gray(`from ${this.inputPath}${suffix}`)}`, ); } } else if (this.isDryRun) { return; } let templateBenchmarkDir = this.bench.get("Template make parent directory"); templateBenchmarkDir.before(); this.fsManager.createDirectoryForFileSync(outputPath); templateBenchmarkDir.after(); if (!Buffer.isBuffer(finalContent) && typeof finalContent !== "string") { throw new Error( `The return value from the render function for the ${this.engine.name} template was not a String or Buffer. Received ${finalContent}`, ); } let templateBenchmark = this.bench.get("Template Write"); templateBenchmark.before(); this.fsManager.writeFileSync(outputPath, finalContent); templateBenchmark.after(); this.writeCount++; debug(`${outputPath} ${lang.finished}.`); let ret = { inputPath: this.inputPath, outputPath: outputPath, url, content: finalContent, rawInput, }; if (data && this.config.dataFilterSelectors?.size > 0) { ret.data = this.retrieveDataForJsonOutput(data, this.config.dataFilterSelectors); } return ret; } async #renderPageEntryWithLayoutsAndTransforms(pageEntry) { // Don’t run linters/transforms/layouts if we didn’t render (via incremental)! if (pageEntry.template.isDryRun && pageEntry.template.isIncremental) { return pageEntry.templateContent; } let content; let layoutKey = pageEntry.data[this.config.keys.layout]; if (this.engine.useLayouts() && layoutKey) { let layout = pageEntry.template.getLayout(layoutKey); content = await layout.renderPageEntry(pageEntry); } else { content = pageEntry.templateContent; } await this.runLinters(content, pageEntry); content = await this.runTransforms(content, pageEntry); return content; } async renderPageEntry(pageEntry) { // @cachedproperty if (!pageEntry.template._cacheRenderedTransformsAndLayoutsPromise) { pageEntry.template._cacheRenderedTransformsAndLayoutsPromise = this.#renderPageEntryWithLayoutsAndTransforms(pageEntry); } return pageEntry.template._cacheRenderedTransformsAndLayoutsPromise; } retrieveDataForJsonOutput(data, selectors) { // if "*" is in the selectors, return all data unfiltered. if (selectors.has("*")) { return data; } let filtered = {}; for (let selector of selectors) { let value = lodashGet(data, selector); lodashSet(filtered, selector, value); } return filtered; } async generateMapEntry(mapEntry, to) { let ret = []; for (let page of mapEntry._pages) { let content; // Note that behavior.render is overridden when using json output if (page.template.isRenderable()) { // this reuses page.templateContent, it doesn’t render it content = await page.template.renderPageEntry(page); } if (to === "json") { let obj = { url: page.url, inputPath: page.inputPath, outputPath: page.outputPath, rawInput: page.rawInput, content: content, }; if (this.config.dataFilterSelectors?.size > 0) { obj.data = this.retrieveDataForJsonOutput(page.data, this.config.dataFilterSelectors); } // json ret.push(obj); continue; } if (!page.template.isRenderable()) { debug("Template not written %o from %o.", page.outputPath, page.template.inputPath); continue; } if (!page.template.behavior.isWriteable()) { debug( "Template not written %o from %o (via permalink: false, permalink.build: false, or a permalink object without a build property).", page.outputPath, page.template.inputPath, ); continue; } // compile returned undefined if (content !== undefined) { ret.push(this._write(page, content)); } } return Promise.all(ret); } async clone() { // TODO do we need to even run the constructor here or can we simplify it even more let tmpl = new Template( this.inputPath, this.templateData, this.extensionMap, this.eleventyConfig, ); // We use this cheap property setter below instead // await tmpl.getTemplateRender(); // preserves caches too, e.g. _frontMatterDataCache // Does not yet include .computedData for (let key in this) { tmpl[key] = this[key]; } return tmpl; } getWriteCount() { return this.writeCount; } getRenderCount() { return this.renderCount; } getInputFileStat() { // @cachedproperty if (!this.#stats) { this.#stats = statSync(this.inputPath); } return this.#stats; } async _getDateInstance(key = "birthtimeMs") { let stat = this.getInputFileStat(); // Issue 1823: https://github.com/11ty/eleventy/issues/1823 // return current Date in a Lambda // otherwise ctime would be "1980-01-01T00:00:00.000Z" // otherwise birthtime would be "1970-01-01T00:00:00.000Z" if (stat.birthtimeMs === 0) { return new Date(); } let newDate = new Date(stat[key]); debug( "Template date: using file’s %o for %o of %o (from %o)", key, this.inputPath, newDate, stat.birthtimeMs, ); return newDate; } async getMappedDate(data) { let dateValue = data?.date; // These can return a Date object, or a string. // Already type checked to be functions in UserConfig for (let fn of this.config.customDateParsing) { let ret = fn.call( { page: data.page, }, dateValue, ); if (ret) { debug("getMappedDate: date value override via `addDateParsing` callback to %o", ret); dateValue = ret; } } if (dateValue) { debug("getMappedDate: using a date in the data for %o of %o", this.inputPath, data.date); if (dateValue?.constructor?.name === "DateTime") { // a luxon instance debug("getMappedDate: found DateTime instance: %o", dateValue); return dateValue.toJSDate(); } if (dateValue instanceof Date) { // YAML does its own date parsing debug("getMappedDate: found Date instance (maybe from YAML): %o", dateValue); return dateValue; } if (typeof dateValue !== "string") { throw new Error( `Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}. Expected a JavaScript Date instance, luxon DateTime instance, or String value.`, ); } // special strings if (!this.isVirtualTemplate()) { if (dateValue.toLowerCase() === "git last modified") { let timestamp = await getUpdatedTimestamp(this.inputPath); if (timestamp) { debug( `getMappedDate: found git last modified timestamp for ${this.inputPath}: %o`, timestamp, ); return new Date(timestamp); } // return now if this file is not yet available in `git` return new Date(); } if (dateValue.toLowerCase() === "last modified") { return this._getDateInstance("ctimeMs"); } if (dateValue.toLowerCase() === "git created") { let timestamp = await getCreatedTimestamp(this.inputPath); if (timestamp) { debug( `getMappedDate: found git created timestamp for ${this.inputPath}: %o`, timestamp, ); return new Date(timestamp); } // return now if this file is not yet available in `git` return new Date(); } if (dateValue.toLowerCase() === "created") { return this._getDateInstance("birthtimeMs"); } } // try to parse with Luxon return fromISOtoDateUTC(dateValue, this.inputPath); } // No Date supplied in the Data Cascade, try to find the date in the file name let filepathRegex = this.inputPath.match(/(\d{4}-\d{2}-\d{2})/); if (filepathRegex !== null) { // if multiple are found in the path, use the first one for the date let dateObj = fromISOtoDateUTC(filepathRegex[1], this.inputPath); debug( "getMappedDate: using filename regex time for %o of %o: %o", this.inputPath, filepathRegex[1], dateObj, ); return dateObj; } // No Date supplied in the Data Cascade if (this.isVirtualTemplate()) { return new Date(); } return this._getDateInstance("birthtimeMs"); } // Important reminder: Template data is first generated in TemplateMap async getTemplateMapEntries(data) { debugDev("%o getMapped()", this.inputPath); this.behavior.setRenderViaDataCascade(data); let entries = []; // does not return outputPath or url, we don’t want to render permalinks yet entries.push({ template: this, inputPath: this.inputPath, data, }); return entries; } } export default Template; ================================================ FILE: src/TemplateBehavior.js ================================================ import { isPlainObject } from "@11ty/eleventy-utils"; class TemplateBehavior { #isRenderOptional; constructor(config) { this.render = true; this.write = true; this.outputFormat = null; if (!config) { throw new Error("Missing config argument in TemplateBehavior"); } this.config = config; } // Render override set to false isRenderableDisabled() { return this.renderableOverride === false; } isRenderableOptional() { return this.#isRenderOptional; } // undefined (fallback), true, false setRenderableOverride(renderableOverride) { if (renderableOverride === "optional") { this.#isRenderOptional = true; this.renderableOverride = undefined; } else { this.#isRenderOptional = false; this.renderableOverride = renderableOverride; } } // permalink *has* a build key or output is json isRenderable() { return this.renderableOverride ?? (this.render || this.isRenderForced()); } setOutputFormat(format) { this.outputFormat = format; } isRenderForced() { return this.outputFormat === "json"; } isWriteable() { return this.write; } // Duplicate logic with TemplatePermalink constructor setRenderViaDataCascade(data) { // render is false *only* if `build` key does not exist in permalink objects (both in data and eleventyComputed) // (note that permalink: false means it won’t write but will still render) let keys = new Set(); if (isPlainObject(data.permalink)) { for (let key of Object.keys(data.permalink)) { keys.add(key); } } let computedKey = this.config.keys.computed; if (computedKey in data && isPlainObject(data[computedKey]?.permalink)) { for (let key of Object.keys(data[computedKey].permalink)) { keys.add(key); } } if (keys.size) { this.render = keys.has("build"); } } setFromPermalink(templatePermalink) { // this.render is duplicated between TemplatePermalink and `setRenderViaDataCascade` above this.render = templatePermalink._isRendered; this.write = templatePermalink._writeToFileSystem; } } export default TemplateBehavior; ================================================ FILE: src/TemplateCollection.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import TemplateData from "./Data/TemplateData.js"; import Sortable from "./Util/Objects/Sortable.js"; import { isGlobMatch } from "./Util/GlobMatcher.js"; class TemplateCollection extends Sortable { constructor() { super(); this._filteredByGlobsCache = new Map(); } getAll() { return this.items.slice(); } getAllSorted() { return this.sort(Sortable.sortFunctionDateInputPath); } getSortedByDate() { return this.sort(Sortable.sortFunctionDate); } getGlobs(globs) { if (typeof globs === "string") { globs = [globs]; } globs = globs.map((glob) => TemplatePath.addLeadingDotSlash(glob)); return globs; } getFilteredByGlob(globs) { globs = this.getGlobs(globs); let key = globs.join("::"); if (!this._dirty) { // Try to find a pre-sorted list and clone it. if (this._filteredByGlobsCache.has(key)) { return [...this._filteredByGlobsCache.get(key)]; } } else if (this._filteredByGlobsCache.size) { // Blow away cache this._filteredByGlobsCache = new Map(); } let filtered = this.getAllSorted().filter((item) => { return isGlobMatch(item.inputPath, globs); }); this._dirty = false; this._filteredByGlobsCache.set(key, [...filtered]); return filtered; } getFilteredByTag(tagName) { return this.getAllSorted().filter((item) => { if (!tagName || TemplateData.getIncludedTagNames(item.data).includes(tagName)) { return true; } return false; }); } getFilteredByTags(...tags) { return this.getAllSorted().filter((item) => { let itemTags = new Set(TemplateData.getIncludedTagNames(item.data)); return tags.every((requiredTag) => { return itemTags.has(requiredTag); }); }); } } export default TemplateCollection; ================================================ FILE: src/TemplateConfig.js ================================================ import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import chalk from "./Adapters/Packages/chalk.js"; import getDefaultConfig from "./Adapters/getDefaultConfig.js"; import { EleventyImportRaw } from "./Util/Require.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import UserConfig from "./UserConfig.js"; import GlobalDependencyMap from "./GlobalDependencyMap.js"; import ExistsCache from "./Util/ExistsCache.js"; import eventBus from "./EventBus.js"; import ProjectTemplateFormats from "./Util/ProjectTemplateFormats.js"; import { isTypeScriptSupported } from "./Util/FeatureTests.cjs"; const debug = debugUtil("Eleventy:TemplateConfig"); const debugDev = debugUtil("Dev:Eleventy:TemplateConfig"); /** * @module 11ty/eleventy/TemplateConfig */ /** * Config as used by the template. * @typedef {object} module:11ty/eleventy/TemplateConfig~TemplateConfig~config * @property {String} [pathPrefix] - The path prefix. */ /** * Errors in eleventy config. * @ignore */ class EleventyConfigError extends EleventyBaseError {} /** * Errors in eleventy plugins. * @ignore */ class EleventyPluginError extends EleventyBaseError {} /** * Config for a template. * @ignore * @param {{}} customRootConfig - tbd. * @param {String} projectConfigPath - Path to local project config. */ class TemplateConfig { #templateFormats; #runMode; #configManuallyDefined = false; /** @type {UserConfig} */ #userConfig = new UserConfig(); #existsCache = new ExistsCache(); #usesGraph; #previousBuildModifiedFile; #activeConfigPath; constructor(customRootConfig, projectConfigPath) { /** @type {object} */ this.overrides = {}; /** * @type {String} * @description Path to local project config. * @default .eleventy.js */ if (projectConfigPath !== undefined) { this.#configManuallyDefined = true; if (!projectConfigPath) { // falsy skips config files this.projectConfigPaths = []; } else { this.projectConfigPaths = [projectConfigPath]; } } else { this.projectConfigPaths = [ ".eleventy.js", "eleventy.config.js", "eleventy.config.mjs", "eleventy.config.cjs", ]; if (isTypeScriptSupported()) { this.projectConfigPaths.push("eleventy.config.ts"); this.projectConfigPaths.push("eleventy.config.mts"); this.projectConfigPaths.push("eleventy.config.cts"); } } if (customRootConfig) { /** * @type {object} * @description Custom root config. */ this.customRootConfig = customRootConfig; debug("Warning: Using custom root config!"); } else { this.customRootConfig = null; } this.hasConfigMerged = false; this.isEsm = false; // Wire up exists API to user config this.userConfig.exists = (filePath) => { return this.existsCache.exists(filePath); }; this.userConfig.events.on("eleventy#templateModified", (inputPath, metadata = {}) => { // Might support multiple at some point this.setPreviousBuildModifiedFile(inputPath, metadata); // Issue #3569, set that this file exists in the cache this.#existsCache.set(inputPath, true); }); } setPreviousBuildModifiedFile(inputPath, metadata = {}) { this.#previousBuildModifiedFile = inputPath; } getPreviousBuildModifiedFile() { return this.#previousBuildModifiedFile; } get userConfig() { return this.#userConfig; } get aggregateBenchmark() { return this.userConfig.benchmarks.aggregate; } /* Setter for Logger */ setLogger(logger) { this.logger = logger; this.userConfig.logger = this.logger; } /* Setter for Directories instance */ setDirectories(directories) { this.directories = directories; this.userConfig.directories = directories.getUserspaceInstance(); } /* Setter for TemplateFormats instance */ setTemplateFormats(templateFormats) { this.#templateFormats = templateFormats; } get templateFormats() { if (!this.#templateFormats) { this.#templateFormats = new ProjectTemplateFormats(); } return this.#templateFormats; } /* Backwards compat */ get inputDir() { return this.directories.input; } setRunMode(runMode) { this.#runMode = runMode; } shouldSpiderJavaScriptDependencies() { // not for a standard build return ( (this.#runMode === "watch" || this.#runMode === "serve") && this.userConfig.watchJavaScriptDependencies ); } /** * Normalises local project config file path. * * @method * @returns {String|undefined} - The normalised local project config file path. */ getLocalProjectConfigFile() { let configFiles = this.getLocalProjectConfigFiles(); let configFile = configFiles.find((path) => path && this.existsCache.exists(path)); if (configFile) { return configFile; } } getLocalProjectConfigFiles() { let paths = this.projectConfigPaths; if (paths?.length > 0) { return TemplatePath.addLeadingDotSlashArray(paths.filter((path) => Boolean(path))); } return []; } getActiveConfigPath() { if (!this.#activeConfigPath) { this.#activeConfigPath = this.getLocalProjectConfigFile(); } return this.#activeConfigPath; } setProjectUsingEsm(isEsmProject) { this.isEsm = !!isEsmProject; this.usesGraph.setIsEsm(isEsmProject); } getIsProjectUsingEsm() { return this.isEsm; } /** * Resets the configuration. */ async reset() { this.#existsCache.reset(); debugDev("Resetting configuration: TemplateConfig and UserConfig."); this.userConfig.reset(); this.usesGraph.reset(); // needs to be before forceReloadConfig #3711 // await this.initializeRootConfig(); await this.forceReloadConfig(); // Clear the compile cache eventBus.emit("eleventy.compileCacheReset"); } /** * Resets the configuration while in watch mode. * * @todo Add implementation. */ resetOnWatch() { // nothing yet } hasInitialized() { return this.hasConfigMerged; } /** * Async-friendly init method */ async init(overrides) { this.#activeConfigPath = undefined; // reset await this.initializeRootConfig(); if (overrides) { this.appendToRootConfig(overrides); } this.config = await this.mergeConfig(); this.hasConfigMerged = true; } /** * Force a reload of the configuration object. */ async forceReloadConfig() { this.hasConfigMerged = false; await this.init(); } /** * Returns the config object. * * @returns {{}} - The config object. */ getConfig() { if (!this.hasConfigMerged) { throw new Error("Invalid call to .getConfig(). Needs an .init() first."); } return this.config; } /** * Overwrites the config path. * * @param {String} path - The new config path. */ async setProjectConfigPath(path) { this.#configManuallyDefined = true; if (path !== undefined) { this.projectConfigPaths = [path]; } else { this.projectConfigPaths = []; } if (this.hasConfigMerged) { // merge it again debugDev("Merging in getConfig again after setting the local project config path."); await this.forceReloadConfig(); } } /** * Overwrites the path prefix. * * @param {String} pathPrefix - The new path prefix. */ setPathPrefix(pathPrefix) { if (pathPrefix && pathPrefix !== "/") { debug("Setting pathPrefix to %o", pathPrefix); this.overrides.pathPrefix = pathPrefix; } } /** * Gets the current path prefix denoting the root folder the output will be deployed to * * @returns {String} - The path prefix string */ getPathPrefix() { if (this.overrides.pathPrefix) { return this.overrides.pathPrefix; } if (!this.hasConfigMerged) { throw new Error("Config has not yet merged. Needs `init()`."); } return this.config?.pathPrefix; } /** * Bootstraps the config object. */ async initializeRootConfig() { this.rootConfig = this.customRootConfig; if (!this.rootConfig) { this.rootConfig = await getDefaultConfig(); } if (typeof this.rootConfig === "function") { // Not yet using async in defaultConfig.js this.rootConfig = this.rootConfig.call(this, this.userConfig); } debug("Default Eleventy config %o", this.rootConfig); } /* * Add additional overrides to the root config object, used for testing * * @param {object} - a subset of the return Object from the user’s config file. */ appendToRootConfig(obj) { Object.assign(this.rootConfig, obj); } /* * Process the userland plugins from the Config * * @param {object} - the return Object from the user’s config file. */ async processPlugins({ dir, pathPrefix }) { this.userConfig.dir = dir; this.userConfig.pathPrefix = pathPrefix; // for Nested addPlugin calls, Issue #1925 this.userConfig._enablePluginExecution(); let storedActiveNamespace = this.userConfig.activeNamespace; for (let { plugin, options, pluginNamespace } of this.userConfig.plugins) { try { this.userConfig.activeNamespace = pluginNamespace; await this.userConfig._executePlugin(plugin, options); } catch (e) { let name = this.userConfig._getPluginName(plugin); let namespaces = [storedActiveNamespace, pluginNamespace].filter((entry) => !!entry); let namespaceStr = ""; if (namespaces.length) { namespaceStr = ` (namespace: ${namespaces.join(".")})`; } throw new EleventyPluginError( `Error processing ${name ? `the \`${name}\`` : "a"} plugin${namespaceStr}`, e, ); } } this.userConfig.activeNamespace = storedActiveNamespace; this.userConfig._disablePluginExecution(); } /** * Fetches and executes the local configuration file * * @returns {Promise} merged - The merged config file object. */ async requireLocalConfigFile() { let localConfig = {}; let exportedConfig = {}; let path = this.getActiveConfigPath(); if (this.projectConfigPaths.length > 0 && this.#configManuallyDefined && !path) { throw new EleventyConfigError( "A configuration file was specified but not found: " + this.projectConfigPaths.join(", "), ); } debug(`Merging default config with ${path}`); if (path) { try { let { default: configDefaultReturn, config: exportedConfigObject } = await EleventyImportRaw(path, this.isEsm ? "esm" : "cjs"); exportedConfig = exportedConfigObject || {}; if (this.directories && Object.keys(exportedConfigObject?.dir || {}).length > 0) { debug( "Setting directories via `config.dir` export from config file: %o", exportedConfigObject.dir, ); this.directories.setViaConfigObject(exportedConfigObject.dir); } if (typeof configDefaultReturn === "function") { localConfig = await configDefaultReturn(this.userConfig); } else { localConfig = configDefaultReturn; } // Removed a check for `filters` in 3.0.0-alpha.6 (now using addTransform instead) https://v3.11ty.dev/docs/config/#transforms } catch (err) { let isModuleError = err instanceof Error && (err?.message || "").includes("Cannot find module"); // TODO the error message here is bad and I feel bad (needs more accurate info) return Promise.reject( new EleventyConfigError( `Error in your Eleventy config file '${path}'.` + (isModuleError ? chalk.cyan(" You may need to run `npm install`.") : ""), err, ), ); } } else { debug( "Project config file not found (not an error—skipping). Looked in: %o", this.projectConfigPaths, ); } return { localConfig, exportedConfig, }; } /** * Merges different config files together. * * @returns {Promise} merged - The merged config file. */ async mergeConfig() { let { localConfig, exportedConfig } = await this.requireLocalConfigFile(); // Merge `export const config = {}` with `return {}` in config callback if (isPlainObject(exportedConfig)) { localConfig = Merge(localConfig || {}, exportedConfig); } if (this.directories) { if (Object.keys(this.userConfig.directoryAssignments || {}).length > 0) { debug( "Setting directories via set*Directory configuration APIs %o", this.userConfig.directoryAssignments, ); this.directories.setViaConfigObject(this.userConfig.directoryAssignments); } if (localConfig && Object.keys(localConfig?.dir || {}).length > 0) { debug( "Setting directories via `dir` object return from configuration file: %o", localConfig.dir, ); this.directories.setViaConfigObject(localConfig.dir); } } // `templateFormats` is an override via `setTemplateFormats` if (this.userConfig?.templateFormats) { this.templateFormats.setViaConfig(this.userConfig.templateFormats); } else if (localConfig?.templateFormats || this.rootConfig?.templateFormats) { // Local project config or defaultConfig.js this.templateFormats.setViaConfig( localConfig.templateFormats || this.rootConfig?.templateFormats, ); } // `templateFormatsAdded` is additive via `addTemplateFormats` if (this.userConfig?.templateFormatsAdded) { this.templateFormats.addViaConfig(this.userConfig.templateFormatsAdded); } // prefer Configuration API methods over return object if (this.userConfig?.htmlTemplateEngine !== undefined) { localConfig.htmlTemplateEngine = this.userConfig?.htmlTemplateEngine; } // prefer Configuration API methods over return object if (this.userConfig?.markdownTemplateEngine !== undefined) { localConfig.markdownTemplateEngine = this.userConfig?.markdownTemplateEngine; } let mergedConfig = Merge({}, this.rootConfig, localConfig); // Setup a few properties for plugins: // Set frozen templateFormats mergedConfig.templateFormats = Object.freeze(this.templateFormats.getTemplateFormats()); // Setup pathPrefix set via command line for plugin consumption if (this.overrides.pathPrefix) { mergedConfig.pathPrefix = this.overrides.pathPrefix; } // Returning a falsy value (e.g. "") from user config should reset to the default value. if (!mergedConfig.pathPrefix) { mergedConfig.pathPrefix = this.rootConfig.pathPrefix; } // This is not set in UserConfig.js so that getters aren’t converted to strings // We want to error if someone attempts to use a setter there. if (this.directories) { mergedConfig.directories = this.directories.getUserspaceInstance(); } // Delay processing plugins until after the result of localConfig is returned // But BEFORE the rest of the config options are merged // this way we can pass directories and other template information to plugins await this.userConfig.events.emit("eleventy.beforeConfig", this.userConfig); let pluginsBench = this.aggregateBenchmark.get("Processing plugins in config"); pluginsBench.before(); await this.processPlugins(mergedConfig); pluginsBench.after(); // Template formats added via plugins if (this.userConfig?.templateFormatsAdded) { this.templateFormats.addViaConfig(this.userConfig.templateFormatsAdded); mergedConfig.templateFormats = Object.freeze(this.templateFormats.getTemplateFormats()); } let eleventyConfigApiMergingObject = this.userConfig.getMergingConfigObject(); if ("templateFormats" in eleventyConfigApiMergingObject) { throw new Error( "Internal error: templateFormats should not return from `getMergingConfigObject`", ); } // Overrides are only used by pathPrefix debug("Configuration overrides: %o", this.overrides); Merge(mergedConfig, eleventyConfigApiMergingObject, this.overrides); debug("Current configuration: %o", mergedConfig); // Add to the merged config too mergedConfig.uses = this.usesGraph; return mergedConfig; } /** * @type {GlobalDependencyMap} */ get usesGraph() { if (!this.#usesGraph) { this.#usesGraph = new GlobalDependencyMap(); this.#usesGraph.setIsEsm(this.isEsm); this.#usesGraph.setTemplateConfig(this); } return this.#usesGraph; } /** * @type {GlobalDependencyMap} */ get uses() { if (!this.usesGraph) { throw new Error("The Eleventy Global Dependency Graph has not yet been initialized."); } return this.usesGraph; } /** * @type {ExistsCache} */ get existsCache() { return this.#existsCache; } } export default TemplateConfig; ================================================ FILE: src/TemplateContent.js ================================================ import { readFileSync } from "node:fs"; import matter from "@11ty/gray-matter"; import lodash from "@11ty/lodash-custom"; import { DeepCopy, TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import JavaScriptFrontMatter from "./Engines/FrontMatter/JavaScript.js"; import { EOL } from "./Util/NewLineAdapter.js"; import TemplateData from "./Data/TemplateData.js"; import TemplateRender from "./TemplateRender.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js"; import eventBus from "./EventBus.js"; import { withResolvers } from "./Util/PromiseUtil.js"; const { set: lodashSet } = lodash; const debug = debugUtil("Eleventy:TemplateContent"); const debugDev = debugUtil("Dev:Eleventy:TemplateContent"); class TemplateContentFrontMatterError extends EleventyBaseError {} class TemplateContentCompileError extends EleventyBaseError {} class TemplateContentRenderError extends EleventyBaseError {} class TemplateContent { #initialized = false; #config; #templateRender; #renderPreprocessorEngine; #extensionMap; #configOptions; #frontMatterOptions; constructor(inputPath, templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new Error("Missing or invalid `templateConfig` argument"); } this.eleventyConfig = templateConfig; this.inputPath = inputPath; } async asyncTemplateInitialization() { if (!this.hasTemplateRender()) { await this.getTemplateRender(); } if (this.#initialized) { return; } this.#initialized = true; let preprocessorEngineName = this.templateRender.getPreprocessorEngineName(); if (preprocessorEngineName && this.templateRender.engine.getName() !== preprocessorEngineName) { let engine = await this.templateRender.getEngineByName(preprocessorEngineName); this.#renderPreprocessorEngine = engine; } } resetCachedTemplate({ eleventyConfig }) { this.eleventyConfig = eleventyConfig; } get dirs() { return this.eleventyConfig.directories; } get inputDir() { return this.dirs.input; } get outputDir() { return this.dirs.output; } getResetTypes(types) { if (types) { return Object.assign( { data: false, read: false, render: false, }, types, ); } return { data: true, read: true, render: true, }; } // Called during an incremental build when the template instance is cached but needs to be reset because it has changed resetCaches(types) { types = this.getResetTypes(types); if (types.read) { delete this.readingPromise; delete this.inputContent; delete this._frontMatterDataCache; } if (types.render) { this.#templateRender = undefined; } } get extensionMap() { if (!this.#extensionMap) { throw new Error("Internal error: Missing `extensionMap` in TemplateContent."); } return this.#extensionMap; } set extensionMap(map) { this.#extensionMap = map; } set eleventyConfig(config) { this.#config = config; if (this.#config.constructor.name === "TemplateConfig") { this.#configOptions = this.#config.getConfig(); } else { throw new Error("Tried to get an TemplateConfig but none was found."); } } get eleventyConfig() { if (this.#config.constructor.name === "TemplateConfig") { return this.#config; } throw new Error("Tried to get an TemplateConfig but none was found."); } get config() { if (this.#config.constructor.name === "TemplateConfig" && !this.#configOptions) { this.#configOptions = this.#config.getConfig(); } return this.#configOptions; } get bench() { return this.config.benchmarkManager.get("Aggregate"); } get engine() { return this.templateRender.engine; } get templateRender() { if (!this.hasTemplateRender()) { throw new Error(`\`templateRender\` has not yet initialized on ${this.inputPath}`); } return this.#templateRender; } hasTemplateRender() { return !!this.#templateRender; } async getTemplateRender() { if (!this.#templateRender) { this.#templateRender = new TemplateRender(this.inputPath, this.eleventyConfig); this.#templateRender.extensionMap = this.extensionMap; return this.#templateRender.init().then(() => { return this.#templateRender; }); } return this.#templateRender; } // For monkey patchers get frontMatter() { if (this.frontMatterOverride) { return this.frontMatterOverride; } else { throw new Error( "Unfortunately you’re using code that monkey patched some Eleventy internals and it isn’t async-friendly. Change your code to use the async `read()` method on the template instead!", ); } } // For monkey patchers set frontMatter(contentOverride) { this.frontMatterOverride = contentOverride; } getInputPath() { return this.inputPath; } getInputDir() { return this.inputDir; } isVirtualTemplate() { let def = this.getVirtualTemplateDefinition(); return !!def; } getVirtualTemplateDefinition() { let inputDirRelativeInputPath = this.eleventyConfig.directories.getInputPathRelativeToInputDirectory(this.inputPath); return this.config.virtualTemplates[inputDirRelativeInputPath]; } getFrontMatterParsingOptions() { if (!this.#frontMatterOptions) { this.#frontMatterOptions = DeepCopy( { // Set a project-wide default. // language: "yaml", // Supplementary engines engines: { // Moved to a fork of gray-matter to modernize to js-yaml@4 internally // yaml: yaml.load.bind(yaml), // Backwards compatible with `js` object front matter // https://github.com/11ty/eleventy/issues/2819 javascript: JavaScriptFrontMatter, // Upstream `js` was removed in @11ty/gray-matter@2 js: JavaScriptFrontMatter, node: function () { throw new Error( "The `node` front matter type was a 3.0.0-alpha.x only feature, removed for stable release. Rename to `js` or `javascript` instead!", ); }, }, }, this.config.frontMatterParsingOptions, ); } return this.#frontMatterOptions; } async #read() { let content = await this.inputContent; if (content || content === "") { let tr = await this.getTemplateRender(); if (tr.engine.useJavaScriptImport()) { return { data: {}, content, }; } let options = this.getFrontMatterParsingOptions(); let fm; try { // Added in 3.0, passed along to front matter engines options.filePath = this.inputPath; fm = matter(content, options); } catch (e) { throw new TemplateContentFrontMatterError( `Having trouble reading front matter from template ${this.inputPath}`, e, ); } if (typeof fm.data?.then === "function") { fm.data = await fm.data; } if (options.excerpt && fm.excerpt) { let excerptString = fm.excerpt + (options.excerpt_separator || "---"); if (fm.content.startsWith(excerptString + EOL)) { // with an os-specific newline after excerpt separator fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + EOL).length); } else if (fm.content.startsWith(excerptString + "\n")) { // with a newline (\n) after excerpt separator // This is necessary for some git configurations on windows fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + 1).length); } else if (fm.content.startsWith(excerptString)) { // no newline after excerpt separator fm.content = fm.excerpt + fm.content.slice(excerptString.length); } // alias, defaults to page.excerpt let alias = options.excerpt_alias || "page.excerpt"; lodashSet(fm.data, alias, fm.excerpt); } // For monkey patchers that used `frontMatter` 🤧 // https://github.com/11ty/eleventy/issues/613#issuecomment-999637109 // https://github.com/11ty/eleventy/issues/2710#issuecomment-1373854834 // Removed this._frontMatter monkey patcher help in 3.0.0-alpha.7 return fm; } else { return { data: {}, content: "", excerpt: "", }; } } async read() { if (!this.readingPromise) { if (!this.inputContent) { // @cachedproperty this.inputContent = this.getInputContent(); } // @cachedproperty this.readingPromise = this.#read(); } return this.readingPromise; } /* Incremental builds cache the Template instances (in TemplateWriter) but * these template specific caches are important for Pagination */ static cache(path, content) { this._inputCache.set(TemplatePath.absolutePath(path), content); } static getCached(path) { return this._inputCache.get(TemplatePath.absolutePath(path)); } static deleteFromInputCache(path) { this._inputCache.delete(TemplatePath.absolutePath(path)); } // Used via clone setInputContent(content) { this.inputContent = content; } async getInputContent() { let tr = await this.getTemplateRender(); let virtualTemplateDefinition = this.getVirtualTemplateDefinition(); if (virtualTemplateDefinition) { let { content } = virtualTemplateDefinition; return content; } if ( tr.engine.useJavaScriptImport() && typeof tr.engine.getInstanceFromInputPath === "function" ) { return tr.engine.getInstanceFromInputPath(this.inputPath); } if (!tr.engine.needsToReadFileContents()) { return ""; } let templateBenchmark = this.bench.get("Template Read"); templateBenchmark.before(); let content; if (this.config.useTemplateCache) { content = TemplateContent.getCached(this.inputPath); } if (!content && content !== "") { let contentBuffer = readFileSync(this.inputPath); content = contentBuffer.toString("utf8"); if (this.config.useTemplateCache) { TemplateContent.cache(this.inputPath, content); } } templateBenchmark.after(); return content; } async _testGetFrontMatter() { let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read(); return fm; } async getPreRender() { let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read(); return fm.content; } async #getFrontMatterData() { let fm = await this.read(); // gray-matter isn’t async-friendly but can return a promise from custom front matter if (fm.data instanceof Promise) { fm.data = await fm.data; } let tr = await this.getTemplateRender(); let extraData = await tr.engine.getExtraDataFromFile(this.inputPath); let virtualTemplateDefinition = this.getVirtualTemplateDefinition(); let virtualTemplateData; if (virtualTemplateDefinition) { virtualTemplateData = virtualTemplateDefinition.data; } let data = Object.assign({}, fm.data, extraData, virtualTemplateData); TemplateData.cleanupData(data, { file: this.inputPath, isVirtualTemplate: Boolean(virtualTemplateData), }); return { data, excerpt: fm.excerpt, }; } async getFrontMatterData() { if (!this._frontMatterDataCache) { // @cachedproperty this._frontMatterDataCache = this.#getFrontMatterData(); } return this._frontMatterDataCache; } getEngineNames(engineOverride) { return this.templateRender.getEnginesList(engineOverride); } async getEngineOverride() { return this.getFrontMatterData().then((data) => { return data[this.config.keys.engineOverride]; }); } // checks engines isTemplateCacheable() { if (this.#renderPreprocessorEngine) { return this.#renderPreprocessorEngine.cacheable; } return this.engine.cacheable; } _getCompileCache(str) { // Caches used to be bifurcated based on engine name, now they’re based on inputPath // TODO does `cacheable` need to help inform whether a cache is used here? let inputPathMap = TemplateContent._compileCache.get(this.inputPath); if (!inputPathMap) { inputPathMap = new Map(); TemplateContent._compileCache.set(this.inputPath, inputPathMap); } let cacheable = this.isTemplateCacheable(); let { useCache, key } = this.engine.getCompileCacheKey(str, this.inputPath); // We also tie the compile cache key to the UserConfig instance, to alleviate issues with global template cache // Better to move the cache to the Eleventy instance instead, no? // (This specifically failed I18nPluginTest cases with filters being cached across tests and not having access to each plugin’s options) key = this.eleventyConfig.userConfig._getUniqueId() + key; return [cacheable, key, inputPathMap, useCache]; } async compile(str, options = {}) { let { type, bypassMarkdown, engineOverride } = options; // Must happen before cacheable fetch below // Likely only necessary for Eleventy Layouts, see TemplateMap->initDependencyMap await this.asyncTemplateInitialization(); // this.templateRender is guaranteed here let tr = await this.getTemplateRender(); if (engineOverride !== undefined) { debugDev("%o overriding template engine to use %o", this.inputPath, engineOverride); await tr.setEngineOverride(engineOverride, bypassMarkdown); } else { tr.setUseMarkdown(!bypassMarkdown); } if (bypassMarkdown && !this.engine.needsCompilation(str)) { return function () { return str; }; } debugDev("%o compile() using engine: %o", this.inputPath, tr.engineName); try { let res; if (this.config.useTemplateCache) { let [cacheable, key, cache, useCache] = this._getCompileCache(str); if (cacheable && key) { if (useCache && cache.has(key)) { this.bench.get("(count) Template Compile Cache Hit").incrementCount(); return cache.get(key); } this.bench.get("(count) Template Compile Cache Miss").incrementCount(); // Compile cache is cleared when the resource is modified (below) // Compilation is async, so we eagerly cache a Promise that eventually // resolves to the compiled function let withRes = withResolvers(); res = withRes.resolve; cache.set(key, withRes.promise); } } let typeStr = type ? ` ${type}` : ""; let templateBenchmark = this.bench.get(`Template Compile${typeStr}`); let inputPathBenchmark = this.bench.get(`> Compile${typeStr} > ${this.inputPath}`); templateBenchmark.before(); inputPathBenchmark.before(); let fn = await tr.getCompiledTemplate(str); inputPathBenchmark.after(); templateBenchmark.after(); debugDev("%o getCompiledTemplate function created", this.inputPath); if (this.config.useTemplateCache && res) { res(fn); } return fn; } catch (e) { let [cacheable, key, cache] = this._getCompileCache(str); if (cacheable && key) { cache.delete(key); } debug(`Having trouble compiling template ${this.inputPath}: %O`, str); throw new TemplateContentCompileError( `Having trouble compiling template ${this.inputPath}`, e, ); } } getParseForSymbolsFunction(str) { let engine = this.engine; // Don’t use markdown as the engine to parse for symbols // TODO pass in engineOverride here if (this.#renderPreprocessorEngine) { engine = this.#renderPreprocessorEngine; } if ("parseForSymbols" in engine) { return () => { if (Array.isArray(str)) { return str .filter((entry) => typeof entry === "string") .map((entry) => engine.parseForSymbols(entry)) .flat(); } if (typeof str === "string") { return engine.parseForSymbols(str); } return []; }; } } // used by computed data or for permalink functions async _renderFunction(fn, ...args) { let mixins = Object.assign({}, this.config.javascriptFunctions); let result = await fn.call(mixins, ...args); // normalize Buffer away if returned from permalink if (Buffer.isBuffer(result)) { return result.toString(); } return result; } async renderComputedData(str, data) { if (typeof str === "function") { return this._renderFunction(str, data); } return this._render(str, data, { type: "Computed Data", bypassMarkdown: true, }); } async renderPermalink(permalink, data) { let tr = await this.getTemplateRender(); let permalinkCompilation = tr.engine.permalinkNeedsCompilation(permalink); // No string compilation: // ({ compileOptions: { permalink: "raw" }}) // These mean `permalink: false`, which is no file system writing: // ({ compileOptions: { permalink: false }}) // ({ compileOptions: { permalink: () => false }}) // ({ compileOptions: { permalink: () => (() = > false) }}) if (permalinkCompilation === false && typeof permalink !== "function") { return permalink; } /* Custom `compile` function for permalinks, usage: permalink: function(permalinkString, inputPath) { return async function(data) { return "THIS IS MY RENDERED PERMALINK"; } } */ if (permalinkCompilation && typeof permalinkCompilation === "function") { permalink = await this._renderFunction(permalinkCompilation, permalink, this.inputPath); } // Raw permalink function (in the app code data cascade) if (typeof permalink === "function") { return this._renderFunction(permalink, data); } return this._render(permalink, data, { type: "Permalink", bypassMarkdown: true, }); } async render(str, data, bypassMarkdown) { return this._render(str, data, { type: "Content", bypassMarkdown, }); } _getPaginationLogSuffix(data) { let suffix = []; if ("pagination" in data) { suffix.push(" ("); if (data.pagination.pages) { suffix.push( `${data.pagination.pages.length} page${data.pagination.pages.length !== 1 ? "s" : ""}`, ); } else { suffix.push("Pagination"); } suffix.push(")"); } return suffix.join(""); } async _render(str, data, options = {}) { let { bypassMarkdown, type } = options; try { if (bypassMarkdown && !this.engine.needsCompilation(str)) { return str; } let fn = await this.compile(str, { bypassMarkdown, engineOverride: data[this.config.keys.engineOverride], type, }); if (fn === undefined) { return; } else if (typeof fn !== "function") { throw new Error(`The \`compile\` function did not return a function. Received ${fn}`); } // Benchmark let templateBenchmark = this.bench.get("Render"); let inputPathBenchmark = this.bench.get( `> Render${type ? ` ${type}` : ""} > ${this.inputPath}${this._getPaginationLogSuffix(data)}`, ); templateBenchmark.before(); if (inputPathBenchmark) { inputPathBenchmark.before(); } let rendered = await fn(data); if (inputPathBenchmark) { inputPathBenchmark.after(); } templateBenchmark.after(); debugDev("%o getCompiledTemplate called, rendered content created", this.inputPath); return rendered; } catch (e) { if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { return Promise.reject(e); } else { let tr = await this.getTemplateRender(); let engine = tr.getReadableEnginesList(); debug(`Having trouble rendering ${engine} template ${this.inputPath}: %O`, str); return Promise.reject( new TemplateContentRenderError( `Having trouble rendering ${engine} template ${this.inputPath}`, e, ), ); } } } getExtensionEntries() { return this.engine.extensionEntries; } isFileRelevantToThisTemplate(incrementalFile, metadata = {}) { // always relevant if incremental file not set (build everything) if (!incrementalFile) { return true; } let hasDependencies = this.engine.hasDependencies(incrementalFile); let isRelevant = this.engine.isFileRelevantTo(this.inputPath, incrementalFile); debug( "Test dependencies to see if %o is relevant to %o: %o", this.inputPath, incrementalFile, isRelevant, ); let extensionEntries = this.getExtensionEntries().filter((entry) => !!entry.isIncrementalMatch); if (extensionEntries.length) { for (let entry of extensionEntries) { if ( entry.isIncrementalMatch.call( { inputPath: this.inputPath, isFullTemplate: metadata.isFullTemplate, isFileRelevantToInputPath: isRelevant, doesFileHaveDependencies: hasDependencies, }, incrementalFile, ) ) { return true; } } return false; } else { // Not great way of building all templates if this is a layout, include, JS dependency. // TODO improve this for default template syntaxes // This is the fallback way of determining if something is incremental (no isIncrementalMatch available) // This will be true if the inputPath and incrementalFile are the same if (isRelevant) { return true; } // only return true here if dependencies are not known if (!hasDependencies && !metadata.isFullTemplate) { return true; } } return false; } } TemplateContent._inputCache = new Map(); TemplateContent._compileCache = new Map(); eventBus.on("eleventy.resourceModified", (path) => { // delete from input cache TemplateContent.deleteFromInputCache(path); // delete from compile cache let normalized = TemplatePath.addLeadingDotSlash(path); let compileCache = TemplateContent._compileCache.get(normalized); if (compileCache) { compileCache.clear(); } }); // Used when the configuration file reset https://github.com/11ty/eleventy/issues/2147 eventBus.on("eleventy.compileCacheReset", () => { TemplateContent._compileCache = new Map(); }); export default TemplateContent; ================================================ FILE: src/TemplateFileSlug.js ================================================ import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; class TemplateFileSlug { constructor(inputPath, extensionMap, eleventyConfig) { let inputDir = eleventyConfig.directories.input; if (inputDir) { inputPath = TemplatePath.stripLeadingSubPath(inputPath, inputDir); } this.inputPath = inputPath; this.cleanInputPath = TemplatePath.stripLeadingDotSlash(inputPath); let dirs = this.cleanInputPath.split("/"); this.dirs = dirs; this.dirs.pop(); this.parsed = path.parse(inputPath); this.filenameNoExt = extensionMap.removeTemplateExtension(this.parsed.base); } // `page.filePathStem` see https://v3.11ty.dev/docs/data-eleventy-supplied/#page-variable getFullPathWithoutExtension() { return "/" + TemplatePath.join(...this.dirs, this._getRawSlug()); } _getRawSlug() { let slug = this.filenameNoExt; return this._stripDateFromSlug(slug); } /** Removes dates in the format of YYYY-MM-DD from a given slug string candidate. */ _stripDateFromSlug(slug) { let reg = slug.match(/\d{4}-\d{2}-\d{2}-(.*)/); if (reg) { return reg[1]; } return slug; } // `page.fileSlug` see https://v3.11ty.dev/docs/data-eleventy-supplied/#page-variable getSlug() { let rawSlug = this._getRawSlug(); if (rawSlug === "index") { if (!this.dirs.length) { return ""; } let lastDir = this.dirs[this.dirs.length - 1]; return this._stripDateFromSlug(lastDir); } return rawSlug; } } export default TemplateFileSlug; ================================================ FILE: src/TemplateGlob.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; class TemplateGlob { static normalizePath(...paths) { if (paths[0].charAt(0) === "!") { throw new Error( `TemplateGlob.normalizePath does not accept ! glob paths like: ${paths.join("")}`, ); } return TemplatePath.addLeadingDotSlash(TemplatePath.join(...paths)); } static normalize(path) { path = path.trim(); if (path.charAt(0) === "!") { return "!" + TemplateGlob.normalizePath(path.slice(1)); } else { return TemplateGlob.normalizePath(path); } } static map(files) { if (typeof files === "string") { return TemplateGlob.normalize(files); } else if (Array.isArray(files)) { return files.map(function (path) { return TemplateGlob.normalize(path); }); } else { return files; } } } export default TemplateGlob; ================================================ FILE: src/TemplateLayout.js ================================================ import { Merge, TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import TemplateLayoutPathResolver from "./TemplateLayoutPathResolver.js"; import TemplateContent from "./TemplateContent.js"; import layoutCache from "./LayoutCache.js"; // const debug = debugUtil("Eleventy:TemplateLayout"); const debugDev = debugUtil("Dev:Eleventy:TemplateLayout"); // https://github.com/11ty/eleventy/issues/3954 class CdataWrapper { static PREFIX = ""; constructor(pageTemplateSyntax = "", layoutTemplateSyntax = "") { this.isEligible = CdataWrapper.isEligible(pageTemplateSyntax, layoutTemplateSyntax); } // Markdown in Markdown layout only static isEligible(templateSyntax, layoutTemplateSyntax) { return ( templateSyntax.split(",").includes("md") && layoutTemplateSyntax.split(",").includes("md") ); } wrap(content) { if (this.isEligible) { return CdataWrapper.PREFIX + content + CdataWrapper.POSTFIX; } return content; } unwrap(content) { if (this.isEligible) { return content.replaceAll(CdataWrapper.PREFIX, "").replaceAll(CdataWrapper.POSTFIX, ""); } return content; } } class TemplateLayout extends TemplateContent { constructor(key, extensionMap, eleventyConfig) { if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") { throw new Error("Expected `eleventyConfig` in TemplateLayout constructor."); } let resolver = new TemplateLayoutPathResolver(key, extensionMap, eleventyConfig); let resolvedPath = resolver.getFullPath(); super(resolvedPath, eleventyConfig); if (!extensionMap) { throw new Error("Expected `extensionMap` in TemplateLayout constructor."); } this.extensionMap = extensionMap; this.key = resolver.getNormalizedLayoutKey(); this.dataKeyLayoutPath = key; this.inputPath = resolvedPath; } getKey() { return this.key; } getFullKey() { return TemplateLayout.resolveFullKey(this.dataKeyLayoutPath, this.inputDir); } getCacheKeys() { return new Set([this.dataKeyLayoutPath, this.getFullKey(), this.key]); } static resolveFullKey(key, inputDir) { return TemplatePath.join(inputDir, key); } static getTemplate(key, eleventyConfig, extensionMap) { let config = eleventyConfig.getConfig(); if (!config.useTemplateCache) { return new TemplateLayout(key, extensionMap, eleventyConfig); } let inputDir = eleventyConfig.directories.input; let fullKey = TemplateLayout.resolveFullKey(key, inputDir); if (!layoutCache.has(fullKey)) { let layout = new TemplateLayout(key, extensionMap, eleventyConfig); layoutCache.add(layout); debugDev("Added %o to LayoutCache", key); return layout; } return layoutCache.get(fullKey); } async getTemplateLayoutMapEntry() { let { data: frontMatterData } = await this.getFrontMatterData(); return { // Used by `TemplateLayout.getTemplate()` key: this.dataKeyLayoutPath, // used by `this.getData()` frontMatterData, }; } async #getTemplateLayoutMap() { // For both the eleventy.layouts event and cyclical layout chain checking (e.g., a => b => c => a) let layoutChain = new Set(); layoutChain.add(this.inputPath); let cfgKey = this.config.keys.layout; let map = []; let mapEntry = await this.getTemplateLayoutMapEntry(); map.push(mapEntry); while (mapEntry.frontMatterData && cfgKey in mapEntry.frontMatterData) { // Layout of the current layout let parentLayoutKey = mapEntry.frontMatterData[cfgKey]; let layout = TemplateLayout.getTemplate( parentLayoutKey, this.eleventyConfig, this.extensionMap, ); // Abort if a circular layout chain is detected. Otherwise, we'll time out and run out of memory. if (layoutChain.has(layout.inputPath)) { throw new Error( `Your layouts have a circular reference, starting at ${map[0].key}! The layout at ${layout.inputPath} was specified twice in this layout chain.`, ); } // Keep track of this layout so we can detect duplicates in subsequent iterations layoutChain.add(layout.inputPath); // reassign for next loop mapEntry = await layout.getTemplateLayoutMapEntry(); map.push(mapEntry); } this.layoutChain = Array.from(layoutChain); return map; } async getTemplateLayoutMap() { if (!this.cachedLayoutMap) { this.cachedLayoutMap = this.#getTemplateLayoutMap(); } return this.cachedLayoutMap; } async getLayoutChain() { if (!Array.isArray(this.layoutChain)) { await this.getTemplateLayoutMap(); } return this.layoutChain; } async #getData() { let map = await this.getTemplateLayoutMap(); let dataToMerge = []; for (let j = map.length - 1; j >= 0; j--) { dataToMerge.push(map[j].frontMatterData); } // Deep merge of layout front matter let data = Merge({}, ...dataToMerge); delete data[this.config.keys.layout]; return data; } async getData() { if (!this.dataCache) { this.dataCache = this.#getData(); } return this.dataCache; } async #getCachedCompiledLayoutFunction() { let rawInput = await this.getPreRender(); return this.compile(rawInput); } // Do only cache this layout’s render function and delegate the rest to the other templates. async getCachedCompiledLayoutFunction() { if (!this.cachedCompiledLayoutFunction) { this.cachedCompiledLayoutFunction = this.#getCachedCompiledLayoutFunction(); } return this.cachedCompiledLayoutFunction; } async getCompiledLayoutFunctions() { let layoutMap = await this.getTemplateLayoutMap(); let fns = []; try { fns.push({ inputPath: this.inputPath, template: this, render: await this.getCachedCompiledLayoutFunction(), }); if (layoutMap.length > 1) { let [, /*currentLayout*/ parentLayout] = layoutMap; let { key } = parentLayout; let layoutTemplate = TemplateLayout.getTemplate( key, this.eleventyConfig, this.extensionMap, ); // The parent already includes the rest of the layout chain let upstreamFns = await layoutTemplate.getCompiledLayoutFunctions(); for (let j = 0, k = upstreamFns.length; j < k; j++) { fns.push(upstreamFns[j]); } } return fns; } catch (e) { debugDev("Clearing LayoutCache after error."); layoutCache.clear(); throw e; } } async render() { throw new Error("Internal error: `render` was removed from TemplateLayout.js in Eleventy 3.0."); } // Inefficient? We want to compile all the templatelayouts into a single reusable callback? // Trouble: layouts may need data variables present downstream/upstream // This is called from Template->renderPageEntry async renderPageEntry(pageEntry) { let pageTemplateSyntax = pageEntry.template?.getEngineNames( pageEntry.data[this.config.keys.engineOverride], ); let templateContent = pageEntry.templateContent; let compiledFunctions = await this.getCompiledLayoutFunctions(); for (let { render, template } of compiledFunctions) { let layoutTemplateSyntax = template.getEngineNames(); // templateEngineOverride not supported in layouts let cdata = new CdataWrapper(pageTemplateSyntax, layoutTemplateSyntax); let data = { ...pageEntry.data, // This should come *after* data, so `content` have override `content` props set in data cascade content: cdata.wrap(templateContent), }; templateContent = cdata.unwrap(await render(data)); } // Don’t set `templateContent` on pageEntry because collection items should not have layout markup return templateContent; } resetCaches(types) { super.resetCaches(types); delete this.dataCache; delete this.layoutChain; delete this.cachedLayoutMap; delete this.cachedCompiledLayoutFunction; } } export default TemplateLayout; ================================================ FILE: src/TemplateLayoutPathResolver.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; // import debugUtil from "debug"; // const debug = debugUtil("Eleventy:TemplateLayoutPathResolver"); class TemplateLayoutPathResolver { constructor(path, extensionMap, templateConfig) { if (!templateConfig) { throw new Error("Expected `templateConfig` in TemplateLayoutPathResolver constructor"); } this.templateConfig = templateConfig; this.originalPath = path; this.originalDisplayPath = TemplatePath.join(this.layoutsDir, this.originalPath) + ` (via \`layout: ${this.originalPath}\`)`; // for error messaging this.path = path; this.aliases = {}; this.extensionMap = extensionMap; if (!extensionMap) { throw new Error("Expected `extensionMap` in TemplateLayoutPathResolver constructor."); } this.init(); } getVirtualTemplate(layoutPath) { let inputDirRelativePath = this.templateConfig.directories.getLayoutPathRelativeToInputDirectory(layoutPath); return this.config.virtualTemplates[inputDirRelativePath]; } get dirs() { return this.templateConfig.directories; } get inputDir() { return this.dirs.input; } get layoutsDir() { return this.dirs.layouts || this.dirs.includes; } /* Backwards compat */ getLayoutsDir() { return this.layoutsDir; } setAliases() { this.aliases = Object.assign({}, this.config.layoutAliases, this.aliases); } // for testing set config(cfg) { this._config = cfg; this.init(); } get config() { if (!this.templateConfig) { throw new Error("Internal error: Missing this.templateConfig"); } return this.templateConfig.getConfig(); } exists(layoutPath) { if (this.getVirtualTemplate(layoutPath)) { return true; } let fullPath = this.templateConfig.directories.getLayoutPath(layoutPath); let existsCache = this.templateConfig.existsCache; if (existsCache.exists(fullPath) && !existsCache.isDirectory(fullPath)) { // #4191 return true; } return false; } init() { // we might be able to move this into the constructor? this.aliases = Object.assign({}, this.config.layoutAliases, this.aliases); if (this.aliases[this.path]) { this.path = this.aliases[this.path]; } let useLayoutResolution = this.config.layoutResolution; if (this.path.split(".").length > 0 && this.exists(this.path)) { this.filename = this.path; this.fullPath = this.templateConfig.directories.getLayoutPath(this.path); } else if (useLayoutResolution) { this.filename = this.findFileName(); this.fullPath = this.templateConfig.directories.getLayoutPath(this.filename || ""); } } addLayoutAlias(from, to) { this.aliases[from] = to; } getFileName() { if (!this.filename) { throw new Error( `You’re trying to use a layout that does not exist: ${this.originalDisplayPath}`, ); } return this.filename; } getFullPath() { if (!this.filename) { throw new Error( `You’re trying to use a layout that does not exist: ${this.originalDisplayPath}`, ); } return this.fullPath; } findFileName() { for (let filename of this.extensionMap.getFileList(this.path)) { if (this.exists(filename)) { return filename; } } } getNormalizedLayoutKey() { return TemplatePath.stripLeadingSubPath(this.fullPath, this.layoutsDir); } } export default TemplateLayoutPathResolver; ================================================ FILE: src/TemplateMap.js ================================================ import { isPlainObject, TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import TemplateCollection from "./TemplateCollection.js"; import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js"; import UsingCircularTemplateContentReferenceError from "./Errors/UsingCircularTemplateContentReferenceError.js"; import DuplicatePermalinkOutputError from "./Errors/DuplicatePermalinkOutputError.js"; import TemplateData from "./Data/TemplateData.js"; import GlobalDependencyMap from "./GlobalDependencyMap.js"; const debug = debugUtil("Eleventy:TemplateMap"); // These template URL filenames are allowed to exclude file extensions const EXTENSIONLESS_URL_ALLOWLIST = [ "/_redirects", // Netlify specific "/.htaccess", // Apache "/_headers", // Cloudflare ]; // must match TemplateDepGraph const SPECIAL_COLLECTION_NAMES = { keys: "[keys]", all: "all", }; class TemplateMap { #dependencyMapInitialized = false; constructor(eleventyConfig) { if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") { throw new Error("Missing or invalid `eleventyConfig` argument."); } this.eleventyConfig = eleventyConfig; this.map = []; this.inputPathMap = new Map(); // NEW: O(1) lookup Map for performance this.collectionsData = null; this.cached = false; this.verboseOutput = true; this.collection = new TemplateCollection(); } set userConfig(config) { this._userConfig = config; } get userConfig() { if (!this._userConfig) { // TODO use this.config for this, need to add collections to mergeable props in userconfig this._userConfig = this.eleventyConfig.userConfig; } return this._userConfig; } get config() { if (!this._config) { this._config = this.eleventyConfig.getConfig(); } return this._config; } async add(template) { if (!template) { return; } let data = await template.getData(); let entries = await template.getTemplateMapEntries(data); let { skippedVia } = await template.runPreprocessors(data); if (skippedVia) { return; } for (let map of entries) { this.map.push(map); this._addToInputPathMap(map); // NEW: Add to lookup Map for O(1) access } } getMap() { return this.map; } _addToInputPathMap(mapEntry) { // Store under absolute path let absoluteInputPath = TemplatePath.absolutePath(mapEntry.inputPath); this.inputPathMap.set(absoluteInputPath, mapEntry); } getTagTarget(str) { if (str === "collections") { // special, means targeting `collections` specifically return SPECIAL_COLLECTION_NAMES.keys; } if (str.startsWith("collections.")) { return str.slice("collections.".length); } // Fixes #2851 if (str.startsWith("collections['") || str.startsWith('collections["')) { return str.slice("collections['".length, -2); } } getPaginationTagTarget(entry) { if (entry.data.pagination?.data) { return this.getTagTarget(entry.data.pagination.data); } } #addEntryToGlobalDependencyGraph(entry) { let consumes = []; consumes.push(this.getPaginationTagTarget(entry)); if (Array.isArray(entry.data.eleventyImport?.collections)) { for (let tag of entry.data.eleventyImport.collections) { consumes.push(tag); } } // Important: consumers must come before publishers // TODO it’d be nice to set the dependency relationship for addCollection here // But collections are not yet populated (they populate after template order) let publishes = TemplateData.getIncludedCollectionNames(entry.data); this.config.uses.addNewNodeRelationships(entry.inputPath, consumes, publishes); } addAllToGlobalDependencyGraph() { this.#dependencyMapInitialized = true; // Should come before individual entry additions this.config.uses.initializeUserConfigurationApiCollections(); for (let entry of this.map) { this.#addEntryToGlobalDependencyGraph(entry); } } async setCollectionByTagName(tagName) { if (this.isUserConfigCollectionName(tagName)) { // async this.collectionsData[tagName] = await this.getUserConfigCollection(tagName); } else { this.collectionsData[tagName] = this.getTaggedCollection(tagName); } let precompiled = this.config.precompiledCollections; if (precompiled?.[tagName]) { if ( tagName === "all" || !Array.isArray(this.collectionsData[tagName]) || this.collectionsData[tagName].length === 0 ) { this.collectionsData[tagName] = precompiled[tagName]; } } } // TODO(slightlyoff): major bottleneck async initDependencyMap(fullTemplateOrder) { // Temporary workaround for async constructor work in templates // Issue #3170 #3870 let inputPathSet = new Set(fullTemplateOrder); await Promise.all( this.map .filter(({ inputPath }) => { return inputPathSet.has(inputPath); }) .map(({ template }) => { // This also happens for layouts in TemplateContent->compile return template.asyncTemplateInitialization(); }), ); for (let depEntry of fullTemplateOrder) { if (GlobalDependencyMap.isCollection(depEntry)) { let tagName = GlobalDependencyMap.getTagName(depEntry); // [keys] should initialize `all` if (tagName === SPECIAL_COLLECTION_NAMES.keys) { await this.setCollectionByTagName("all"); // [NAME] is special and implied (e.g. [keys]) } else if (!tagName.startsWith("[") && !tagName.endsWith("]")) { // is a tag (collection) entry await this.setCollectionByTagName(tagName); } continue; } // is a template entry let map = this.getMapEntryForInputPath(depEntry); await this.#initDependencyMapEntry(map); } } async #initDependencyMapEntry(map) { try { map._pages = await map.template.getTemplates(map.data); } catch (e) { throw new Error("Error generating template page(s) for " + map.inputPath + ".", { cause: e }); } if (map._pages.length === 0) { // Reminder: a serverless code path was removed here. } else { let counter = 0; for (let page of map._pages) { // Copy outputPath to map entry // This is no longer used internally, just for backwards compatibility // Error added in v3 for https://github.com/11ty/eleventy/issues/3183 if (map.data.pagination) { if (!Object.prototype.hasOwnProperty.call(map, "outputPath")) { Object.defineProperty(map, "outputPath", { get() { throw new Error( "Internal error: `.outputPath` on a paginated map entry is not consistent. Use `_pages[…].outputPath` instead.", ); }, }); } } else if (!map.outputPath) { map.outputPath = page.outputPath; } if (counter === 0 || map.data.pagination?.addAllPagesToCollections) { if (map.data.eleventyExcludeFromCollections !== true) { // is in *some* collections this.collection.add(page); } } counter++; } } } getTemplateOrder() { // 1. Templates that don’t use Pagination // 2. Pagination templates that consume config API collections // 3. Pagination templates consuming `collections` // 4. Pagination templates consuming `collections.all` let fullTemplateOrder = this.config.uses.getTemplateOrder(); return fullTemplateOrder .map((entry) => { if (GlobalDependencyMap.isCollection(entry)) { return entry; } let inputPath = TemplatePath.addLeadingDotSlash(entry); if (!this.hasMapEntryForInputPath(inputPath)) { return false; } return inputPath; }) .filter(Boolean); } async cache() { if (!this.#dependencyMapInitialized) { this.addAllToGlobalDependencyGraph(); } this.collectionsData = {}; for (let entry of this.map) { entry.data.collections = this.collectionsData; } let fullTemplateOrder = this.getTemplateOrder(); debug( "Rendering templates in order (%o concurrency): %O", this.userConfig.getConcurrency(), fullTemplateOrder, ); await this.initDependencyMap(fullTemplateOrder); await this.resolveRemainingComputedData(); let orderedPaths = this.#removeTagsFromTemplateOrder(fullTemplateOrder); let orderedMap = orderedPaths.map((inputPath) => { return this.getMapEntryForInputPath(inputPath); }); await this.config.events.emitLazy("eleventy.contentMap", () => { return { inputPathToUrl: this.generateInputUrlContentMap(orderedMap), urlToInputPath: this.generateUrlMap(orderedMap), }; }); await this.runDataSchemas(orderedMap); await this.populateContentDataInMap(orderedMap); this.populateCollectionsWithContent(); this.cached = true; this.checkForDuplicatePermalinks(); this.checkForMissingFileExtensions(); await this.config.events.emitLazy("eleventy.layouts", () => this.generateLayoutsMap()); } generateInputUrlContentMap(orderedMap) { let entries = {}; for (let entry of orderedMap) { entries[entry.inputPath] = entry._pages.map((entry) => entry.url); } return entries; } generateUrlMap(orderedMap) { let entries = {}; for (let entry of orderedMap) { for (let page of entry._pages) { // duplicate urls throw an error, so we can return non array here entries[page.url] = { inputPath: entry.inputPath, groupNumber: page.groupNumber, }; } } return entries; } hasMapEntryForInputPath(inputPath) { return Boolean(this.getMapEntryForInputPath(inputPath)); } getMapEntryForInputPath(inputPath) { let absoluteInputPath = TemplatePath.absolutePath(inputPath); return this.inputPathMap.get(absoluteInputPath); } #removeTagsFromTemplateOrder(maps) { return maps.filter((dep) => !GlobalDependencyMap.isCollection(dep)); } async runDataSchemas(orderedMap) { for (let map of orderedMap) { if (!map._pages) { continue; } for (let pageEntry of map._pages) { // Data Schema callback #879 if (typeof pageEntry.data[this.config.keys.dataSchema] === "function") { try { await pageEntry.data[this.config.keys.dataSchema](pageEntry.data); } catch (e) { throw new Error( `Error in the data schema for: ${map.inputPath} (via \`eleventyDataSchema\`)`, { cause: e }, ); } } } } } async populateContentDataInMap(orderedMap) { let usedTemplateContentTooEarlyMap = []; // Note that empty pagination templates will be skipped here as not renderable let filteredMap = orderedMap.filter((entry) => entry.template.isRenderable()); // Get concurrency level from user config const concurrency = this.userConfig.getConcurrency(); // Process the templates in chunks to limit concurrency // This replaces the functionality of p-map's concurrency option for (let i = 0; i < filteredMap.length; i += concurrency) { // Create a chunk of tasks that will run in parallel const chunk = filteredMap.slice(i, i + concurrency); // Run the chunk of tasks in parallel await Promise.all( chunk.map(async (map) => { if (!map._pages) { throw new Error(`Internal error: _pages not found for ${map.inputPath}`); } // IMPORTANT: this is where template content is rendered try { for (let pageEntry of map._pages) { pageEntry.templateContent = await pageEntry.template.renderPageEntryWithoutLayout(pageEntry); } } catch (e) { if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { // Add to list of templates that need to be processed again usedTemplateContentTooEarlyMap.push(map); // Reset cached render promise for (let pageEntry of map._pages) { pageEntry.template.resetCaches({ render: true }); } } else { throw e; } } }), ); } // Process templates that had premature template content errors // This is the second pass for templates that couldn't be rendered in the first pass for (let map of usedTemplateContentTooEarlyMap) { try { for (let pageEntry of map._pages) { pageEntry.templateContent = await pageEntry.template.renderPageEntryWithoutLayout(pageEntry); } } catch (e) { if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { // If we still have template content errors after the second pass, // it's likely a circular reference throw new UsingCircularTemplateContentReferenceError( `${map.inputPath} contains a circular reference (using collections) to its own templateContent.`, ); } else { // rethrow? throw e; } } } } getTaggedCollection(tag) { let result; if (!tag || tag === "all") { result = this.collection.getAllSorted(); } else { result = this.collection.getFilteredByTag(tag); } // May not return an array (can be anything) // https://www.11ty.dev/docs/collections-api/#return-values debug(`Collection: collections.${tag || "all"} size: ${result?.length}`); return result; } /* 3.0.0-alpha.1: setUserConfigCollections method removed (was only used for testing) */ isUserConfigCollectionName(name) { let collections = this.userConfig.getCollections(); return name && !!collections[name]; } getUserConfigCollectionNames() { return Object.keys(this.userConfig.getCollections()); } async getUserConfigCollection(name) { let configCollections = this.userConfig.getCollections(); // This works with async now let result = await configCollections[name](this.collection); // May not return an array (can be anything) // https://www.11ty.dev/docs/collections-api/#return-values debug(`Collection: collections.${name} size: ${result?.length}`); return result; } populateCollectionsWithContent() { for (let collectionName in this.collectionsData) { // skip custom collections set in configuration files that have arbitrary types if (!Array.isArray(this.collectionsData[collectionName])) { continue; } for (let item of this.collectionsData[collectionName]) { // skip custom collections set in configuration files that have arbitrary types if (!isPlainObject(item) || !("inputPath" in item)) { continue; } let entry = this.getMapEntryForInputPath(item.inputPath); // This check skips precompiled collections if (entry) { let index = item.pageNumber || 0; let content = entry._pages[index]._templateContent; if (content !== undefined) { item.templateContent = content; } } } } } async resolveRemainingComputedData() { let promises = []; for (let entry of this.map) { for (let pageEntry of entry._pages) { if (this.config.keys.computed in pageEntry.data) { promises.push(pageEntry.template.resolveRemainingComputedData(pageEntry.data)); } } } return Promise.all(promises); } async generateLayoutsMap() { let layouts = {}; for (let entry of this.map) { for (let page of entry._pages) { let tmpl = page.template; if (tmpl.templateUsesLayouts(page.data)) { let layoutKey = page.data[this.config.keys.layout]; let layout = tmpl.getLayout(layoutKey); let layoutChain = await layout.getLayoutChain(); let priors = []; for (let filepath of layoutChain) { if (!layouts[filepath]) { layouts[filepath] = new Set(); } layouts[filepath].add(page.inputPath); for (let prior of priors) { layouts[filepath].add(prior); } priors.push(filepath); } } } } for (let key in layouts) { layouts[key] = Array.from(layouts[key]); } return layouts; } #onEachPage(callback) { for (let template of this.map) { for (let page of template._pages) { callback(page, template); } } } checkForDuplicatePermalinks() { let inputs = {}; let outputPaths = {}; let warnings = {}; this.#onEachPage((page, template) => { if (page.outputPath === false || page.url === false) { // do nothing (also serverless) } else { // Make sure output doesn’t overwrite input (e.g. --input=. --output=.) // Related to https://github.com/11ty/eleventy/issues/3327 if (page.outputPath === page.inputPath) { throw new DuplicatePermalinkOutputError( `The template at "${page.inputPath}" attempted to overwrite itself.`, ); } else if (inputs[page.outputPath]) { throw new DuplicatePermalinkOutputError( `The template at "${page.inputPath}" attempted to overwrite an existing template at "${page.outputPath}".`, ); } inputs[page.inputPath] = true; if (!outputPaths[page.outputPath]) { outputPaths[page.outputPath] = [template.inputPath]; } else { warnings[page.outputPath] = `Output conflict: multiple input files are writing to \`${ page.outputPath }\`. Use distinct \`permalink\` values to resolve this conflict. 1. ${template.inputPath} ${outputPaths[page.outputPath] .map(function (inputPath, index) { return ` ${index + 2}. ${inputPath}\n`; }) .join("")} `; outputPaths[page.outputPath].push(template.inputPath); } } }); let warningList = Object.values(warnings); if (warningList.length) { // throw one at a time throw new DuplicatePermalinkOutputError(warningList[0]); } } checkForMissingFileExtensions() { // disabled in config if (this.userConfig?.errorReporting?.allowMissingExtensions === true) { return; } this.#onEachPage((page) => { if ( page.outputPath === false || page.url === false || page.data.eleventyAllowMissingExtension || EXTENSIONLESS_URL_ALLOWLIST.some((url) => page.url.endsWith(url)) ) { // do nothing (also serverless) } else { if (TemplatePath.getExtension(page.outputPath) === "") { let e = new Error(`The template at '${page.inputPath}' attempted to write to '${page.outputPath}'${page.data.permalink ? ` (via \`permalink\` value: '${page.data.permalink}')` : ""}, which is a target on the file system that does not include a file extension. You *probably* want to add a file extension to your permalink so that hosts will know how to correctly serve this file to web browsers. Without a file extension, this file may not be reliably deployed without additional hosting configuration (it won’t have a mime type) and may also cause local development issues if you later attempt to write to a subdirectory of the same name. Learn more: https://v3.11ty.dev/docs/permalinks/#trailing-slashes This is usually but not *always* an error so if you’d like to disable this error message, add \`eleventyAllowMissingExtension: true\` somewhere in the data cascade for this template or use \`eleventyConfig.configureErrorReporting({ allowMissingExtensions: true });\` to disable this feature globally.`); e.skipOriginalStack = true; throw e; } } }); } // TODO move these into TemplateMapTest.js _testGetAllTags() { let allTags = {}; for (let map of this.map) { let tags = map.data.tags; if (Array.isArray(tags)) { for (let tag of tags) { allTags[tag] = true; } } } return Object.keys(allTags); } async _testGetUserConfigCollectionsData() { let collections = {}; let configCollections = this.userConfig.getCollections(); for (let name in configCollections) { collections[name] = configCollections[name](this.collection); debug(`Collection: collections.${name} size: ${collections[name].length}`); } return collections; } async _testGetTaggedCollectionsData() { let collections = {}; collections.all = this.collection.getAllSorted(); debug(`Collection: collections.all size: ${collections.all.length}`); let tags = this._testGetAllTags(); for (let tag of tags) { collections[tag] = this.collection.getFilteredByTag(tag); debug(`Collection: collections.${tag} size: ${collections[tag].length}`); } return collections; } async _testGetAllCollectionsData() { let collections = {}; let taggedCollections = await this._testGetTaggedCollectionsData(); Object.assign(collections, taggedCollections); let userConfigCollections = await this._testGetUserConfigCollectionsData(); Object.assign(collections, userConfigCollections); return collections; } async _testGetCollectionsData() { if (!this.cached) { await this.cache(); } return this.collectionsData; } } export default TemplateMap; ================================================ FILE: src/TemplatePassthrough.js ================================================ import path from "node:path"; import copy from "@11ty/recursive-copy"; import { TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import { readableFileSize } from "./Util/FileSize.js"; import { isDynamicPattern } from "./Util/GlobMatcher.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js"; import ProjectDirectories from "./Util/ProjectDirectories.js"; const debug = debugUtil("Eleventy:TemplatePassthrough"); class TemplatePassthroughError extends EleventyBaseError {} class TemplatePassthrough { isDryRun = false; #isInputPathGlob; #benchmarks; #isAlreadyNormalized = false; #projectDirCheck = false; // paths already guaranteed from the autocopy plugin static factory(inputPath, outputPath, opts = {}) { let p = new TemplatePassthrough( { inputPath, outputPath, copyOptions: opts.copyOptions, }, opts.templateConfig, ); return p; } constructor(path, templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new Error( "Internal error: Missing `templateConfig` or was not an instance of `TemplateConfig`.", ); } this.templateConfig = templateConfig; this.rawPath = path; // inputPath is relative to the root of your project and not your Eleventy input directory. // TODO normalize these with forward slashes this.inputPath = this.normalizeIfDirectory(path.inputPath); this.#isInputPathGlob = isDynamicPattern(this.inputPath); this.outputPath = path.outputPath; this.copyOptions = path.copyOptions; // custom options for recursive-copy } get benchmarks() { if (!this.#benchmarks) { this.#benchmarks = { aggregate: this.config.benchmarkManager.get("Aggregate"), }; } return this.#benchmarks; } get config() { return this.templateConfig.getConfig(); } get directories() { return this.templateConfig.directories; } // inputDir is used when stripping from output path in `getOutputPath` get inputDir() { return this.templateConfig.directories.input; } get outputDir() { return this.templateConfig.directories.output; } // Skips `getFiles()` normalization setIsAlreadyNormalized(isNormalized) { this.#isAlreadyNormalized = Boolean(isNormalized); } setCheckSourceDirectory(check) { this.#projectDirCheck = Boolean(check); } /* { inputPath, outputPath } though outputPath is *not* the full path: just the output directory */ getPath() { return this.rawPath; } async getOutputPath(inputFileFromGlob) { let { inputDir, outputDir, outputPath, inputPath } = this; if (outputPath === true) { // no explicit target, implied target if (this.isDirectory(inputPath)) { let inputRelativePath = TemplatePath.stripLeadingSubPath( inputFileFromGlob || inputPath, inputDir, ); return ProjectDirectories.normalizeDirectory( TemplatePath.join(outputDir, inputRelativePath), ); } return TemplatePath.normalize( TemplatePath.join( outputDir, TemplatePath.stripLeadingSubPath(inputFileFromGlob || inputPath, inputDir), ), ); } if (inputFileFromGlob) { return this.getOutputPathForGlobFile(inputFileFromGlob); } // Has explicit target // Bug when copying incremental file overwriting output directory (and making it a file) // e.g. public/test.css -> _site // https://github.com/11ty/eleventy/issues/2278 let fullOutputPath = TemplatePath.normalize(TemplatePath.join(outputDir, outputPath)); if (outputPath === "" || this.isDirectory(inputPath)) { fullOutputPath = ProjectDirectories.normalizeDirectory(fullOutputPath); } // TODO room for improvement here: if ( !this.#isInputPathGlob && this.isExists(inputPath) && !this.isDirectory(inputPath) && this.isDirectory(fullOutputPath) ) { let filename = path.parse(inputPath).base; return TemplatePath.normalize(TemplatePath.join(fullOutputPath, filename)); } return fullOutputPath; } async getOutputPathForGlobFile(inputFileFromGlob) { return TemplatePath.join( await this.getOutputPath(), TemplatePath.getLastPathSegment(inputFileFromGlob), ); } setDryRun(isDryRun) { this.isDryRun = Boolean(isDryRun); } setRunMode(runMode) { this.runMode = runMode; } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } async getFiles(glob) { debug("Searching for: %o", glob); let b = this.benchmarks.aggregate.get("Searching the file system (passthrough)"); b.before(); if (!this.fileSystemSearch) { throw new Error("Internal error: Missing `fileSystemSearch` property."); } // TODO perf this globs once per addPassthroughCopy entry let files = TemplatePath.addLeadingDotSlashArray( await this.fileSystemSearch.search("passthrough", glob, { ignore: [ // *only* ignores output dir (not node_modules!) this.outputDir, ], }), ); b.after(); return files; } isExists(filePath) { return this.templateConfig.existsCache.exists(filePath); } isDirectory(filePath) { return this.templateConfig.existsCache.isDirectory(filePath); } // dir is guaranteed to exist by context // dir may not be a directory normalizeIfDirectory(input) { if (typeof input === "string") { if (input.endsWith(path.sep) || input.endsWith("/")) { return input; } // When inputPath is a directory, make sure it has a slash for passthrough copy aliasing // https://github.com/11ty/eleventy/issues/2709 if (this.isDirectory(input)) { return `${input}/`; } } return input; } // maps input paths to output paths async getFileMap() { if (this.#isAlreadyNormalized) { return [ { inputPath: this.inputPath, outputPath: this.outputPath, }, ]; } // TODO VirtualFileSystem candidate if (!isDynamicPattern(this.inputPath) && this.isExists(this.inputPath)) { return [ { inputPath: this.inputPath, outputPath: await this.getOutputPath(), }, ]; } let paths = []; // If not directory or file, attempt to get globs let files = await this.getFiles(this.inputPath); for (let filePathFromGlob of files) { paths.push({ inputPath: filePathFromGlob, outputPath: await this.getOutputPath(filePathFromGlob), }); } return paths; } /* Types: * 1. via glob, individual files found * 2. directory, triggers an event for each file * 3. individual file */ async copy(src, dest, copyOptions) { if (this.#projectDirCheck && !this.directories.isFileInProjectFolder(src)) { return Promise.reject( new TemplatePassthroughError( "Source file is not in the project directory. Check your passthrough paths.", ), ); } if (!this.directories.isFileInOutputFolder(dest)) { return Promise.reject( new TemplatePassthroughError( "Destination is not in the site output directory. Check your passthrough paths.", ), ); } let fileCopyCount = 0; let fileSizeCount = 0; let map = {}; let b = this.benchmarks.aggregate.get("Passthrough Copy File"); // returns a promise return copy(src, dest, copyOptions) .on(copy.events.COPY_FILE_START, (copyOp) => { // Access to individual files at `copyOp.src` map[copyOp.src] = copyOp.dest; b.before(); }) .on(copy.events.COPY_FILE_COMPLETE, (copyOp) => { fileCopyCount++; fileSizeCount += copyOp.stats.size; if (copyOp.stats.size > 5000000) { debug( `Copied %o (⚠️ large) file from %o`, readableFileSize(copyOp.stats.size), copyOp.src, ); } else { debug(`Copied %o file from %o`, readableFileSize(copyOp.stats.size), copyOp.src); } b.after(); }) .then( () => { return { count: fileCopyCount, size: fileSizeCount, map, }; }, (error) => { if (copyOptions.overwrite === false && error.code === "EEXIST") { // just ignore if the output already exists and overwrite: false debug("Overwrite error ignored: %O", error); return { count: 0, size: 0, map, }; } return Promise.reject(error); }, ); } async write() { if (this.isDryRun) { return Promise.resolve({ count: 0, map: {}, }); } debug("Copying %o", this.inputPath); let fileMap = await this.getFileMap(); // default options for recursive-copy // see https://www.npmjs.com/package/recursive-copy#arguments let copyOptionsDefault = { overwrite: true, // overwrite output. fails when input is directory (mkdir) and output is file dot: true, // copy dotfiles junk: false, // copy cache files like Thumbs.db results: false, expand: false, // follow symlinks (matches recursive-copy default) debug: false, // (matches recursive-copy default) // Note: `filter` callback function only passes in a relative path, which is unreliable // See https://github.com/timkendrick/recursive-copy/blob/4c9a8b8a4bf573285e9c4a649a30a2b59ccf441c/lib/copy.js#L59 // e.g. `{ filePaths: [ './img/coolkid.jpg' ], relativePaths: [ '' ] }` }; let copyOptions = Object.assign(copyOptionsDefault, this.copyOptions); let promises = fileMap.map((entry) => { // For-free passthrough copy if (checkPassthroughCopyBehavior(this.config, this.runMode)) { let aliasMap = {}; aliasMap[entry.inputPath] = entry.outputPath; return Promise.resolve({ count: 0, map: aliasMap, }); } // Copy the files (only in build mode) return this.copy(entry.inputPath, entry.outputPath, copyOptions); }); // IMPORTANT: this returns an array of promises, does not await for promise to finish return Promise.all(promises).then( (results) => { // collate the count and input/output map results from the array. let count = 0; let size = 0; let map = {}; for (let result of results) { count += result.count; size += result.size; Object.assign(map, result.map); } return { count, size, map, }; }, (err) => { throw new TemplatePassthroughError(`Error copying passthrough files: ${err.message}`, err); }, ); } } export default TemplatePassthrough; ================================================ FILE: src/TemplatePassthroughManager.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import TemplatePassthrough from "./TemplatePassthrough.js"; import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js"; import { isGlobMatch, isDynamicPattern } from "./Util/GlobMatcher.js"; import { withResolvers } from "./Util/PromiseUtil.js"; const debug = debugUtil("Eleventy:TemplatePassthroughManager"); class TemplatePassthroughManagerCopyError extends EleventyBaseError {} class TemplatePassthroughManager { #isDryRun = false; #afterBuild; #queue = new Map(); #extensionMap; constructor(templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new Error("Internal error: Missing or invalid `templateConfig` argument."); } this.templateConfig = templateConfig; this.config = templateConfig.getConfig(); // eleventy# event listeners are removed on each build this.config.events.on("eleventy#copy", ({ source, target, options }) => { this.enqueueCopy(source, target, options); }); this.config.events.on("eleventy#beforerender", () => { this.#afterBuild = withResolvers(); }); this.config.events.on("eleventy#render", () => { let { resolve } = this.#afterBuild; resolve(); }); this.reset(); } reset() { this.count = 0; this.size = 0; this.conflictMap = {}; this.incrementalFile; this.#queue = new Map(); } set extensionMap(extensionMap) { this.#extensionMap = extensionMap; } get extensionMap() { if (!this.#extensionMap) { throw new Error("Internal error: missing `extensionMap` in TemplatePassthroughManager."); } return this.#extensionMap; } get inputDir() { return this.templateConfig.directories.input; } get outputDir() { return this.templateConfig.directories.output; } setDryRun(isDryRun) { this.#isDryRun = Boolean(isDryRun); } setRunMode(runMode) { this.runMode = runMode; } setIncrementalFile(path) { if (path) { this.incrementalFile = path; } } resetIncrementalFile() { this.incrementalFile = undefined; } _normalizePaths(path, outputPath, copyOptions = {}) { return { inputPath: TemplatePath.addLeadingDotSlash(path), outputPath: outputPath ? TemplatePath.stripLeadingDotSlash(outputPath) : true, copyOptions, }; } getConfigPaths() { let paths = []; let pathsRaw = this.config.passthroughCopies || {}; debug("`addPassthroughCopy` config API paths: %o", pathsRaw); for (let [inputPath, { outputPath, copyOptions }] of Object.entries(pathsRaw)) { paths.push(this._normalizePaths(inputPath, outputPath, copyOptions)); } debug("`addPassthroughCopy` config API normalized paths: %o", paths); return paths; } getConfigPathGlobs() { return this.getConfigPaths().map((path) => { return TemplatePath.convertToRecursiveGlobSync(path.inputPath); }); } getNonTemplatePaths(paths) { let matches = []; for (let path of paths) { if (!this.extensionMap.hasEngine(path)) { matches.push(path); } } return matches; } getCopyCount() { return this.count; } getCopySize() { return this.size; } getMetadata() { return { copyCount: this.getCopyCount(), copySize: this.getCopySize(), }; } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } getTemplatePassthroughForPath(path) { let inst = new TemplatePassthrough(path, this.templateConfig); inst.setFileSystemSearch(this.fileSystemSearch); inst.setDryRun(this.#isDryRun); inst.setRunMode(this.runMode); return inst; } async copyPassthrough(pass) { if (!(pass instanceof TemplatePassthrough)) { throw new TemplatePassthroughManagerCopyError( "copyPassthrough expects an instance of TemplatePassthrough", ); } let { inputPath } = pass.getPath(); // TODO https://github.com/11ty/eleventy/issues/2452 // De-dupe both the input and output paired together to avoid the case // where an input/output pair has been added via multiple passthrough methods (glob, file suffix, etc) // Probably start with the `filter` callback in recursive-copy but it only passes relative paths // See the note in TemplatePassthrough.js->write() // Also note that `recursive-copy` handles repeated overwrite copy to the same destination just fine. // e.g. `for(let j=0, k=1000; j { for (let src in map) { let dest = map[src]; if (this.conflictMap[dest]) { if (src !== this.conflictMap[dest]) { let paths = [src, this.conflictMap[dest]].sort(); throw new TemplatePassthroughManagerCopyError( `Multiple passthrough copy files are trying to write to the same output file (${TemplatePath.standardizeFilePath(dest)}). ${paths.map((p) => TemplatePath.standardizeFilePath(p)).join(" and ")}`, ); } else { // Multiple entries from the same source debug( "A passthrough copy entry (%o) caused the same file (%o) to be copied more than once to the output (%o). This is atomically safe but a waste of build resources.", inputPath, src, dest, ); } } this.conflictMap[dest] = src; } if (pass.isDryRun) { // We don’t count the skipped files as we need to iterate over them debug( "Skipped %o (either from --dryrun or --incremental or for-free passthrough copy)", inputPath, ); } else { if (count) { this.count += count; this.size += size; debug("Copied %o (%d files, %d size)", inputPath, count || 0, size || 0); } else { debug("Skipped copying %o (emulated passthrough copy)", inputPath); } } return { count, map, }; }, function (e) { return Promise.reject( new TemplatePassthroughManagerCopyError(`Having trouble copying '${inputPath}'`, e), ); }, ); } isPassthroughCopyFile(paths, changedFile) { if (!changedFile) { return false; } // passthrough copy by non-matching engine extension (via templateFormats) for (let path of paths) { if (path === changedFile && !this.extensionMap.hasEngine(path)) { return true; } } for (let path of this.getConfigPaths()) { if (TemplatePath.startsWithSubPath(changedFile, path.inputPath)) { return path; } if ( changedFile && isDynamicPattern(path.inputPath) && isGlobMatch(changedFile, [path.inputPath]) ) { return path; } } return false; } getAllNormalizedPaths(paths = []) { if (this.incrementalFile) { let isPassthrough = this.isPassthroughCopyFile(paths, this.incrementalFile); if (isPassthrough) { if (isPassthrough.outputPath) { return [isPassthrough]; } return [this._normalizePaths(this.incrementalFile)]; } // Fixes https://github.com/11ty/eleventy/issues/2491 if (!checkPassthroughCopyBehavior(this.config, this.runMode)) { return []; } } let normalizedPaths = this.getConfigPaths(); if (debug.enabled) { for (let path of normalizedPaths) { debug("TemplatePassthrough copying from config: %o", path); } } if (paths?.length) { let passthroughPaths = this.getNonTemplatePaths(paths); for (let path of passthroughPaths) { let normalizedPath = this._normalizePaths(path); debug( `TemplatePassthrough copying from non-matching file extension: ${normalizedPath.inputPath}`, ); normalizedPaths.push(normalizedPath); } } return normalizedPaths; } // keys: output // values: input getAliasesFromPassthroughResults(result) { let entries = {}; for (let entry of result) { for (let src in entry.map) { let dest = TemplatePath.stripLeadingSubPath(entry.map[src], this.outputDir); entries["/" + encodeURI(dest)] = src; } } return entries; } async #waitForTemplatesRendered() { if (!this.#afterBuild) { return Promise.resolve(); // immediately resolve } let { promise } = this.#afterBuild; return promise; } enqueueCopy(source, target, copyOptions) { let key = `${source}=>${target}`; // light de-dupe the same source/target combo (might be in the same file, might be viaTransforms) if (this.#queue.has(key)) { return; } let passthrough = TemplatePassthrough.factory(source, target, { templateConfig: this.templateConfig, copyOptions, }); passthrough.setCheckSourceDirectory(true); passthrough.setIsAlreadyNormalized(true); passthrough.setRunMode(this.runMode); passthrough.setDryRun(this.#isDryRun); this.#queue.set(key, this.copyPassthrough(passthrough)); } async copyAll(templateExtensionPaths) { debug("TemplatePassthrough copy started."); let normalizedPaths = this.getAllNormalizedPaths(templateExtensionPaths); let passthroughs = normalizedPaths.map((path) => this.getTemplatePassthroughForPath(path)); let promises = passthroughs.map((pass) => this.copyPassthrough(pass)); await this.#waitForTemplatesRendered(); for (let [key, afterBuildCopyPromises] of this.#queue) { promises.push(afterBuildCopyPromises); } return Promise.all(promises).then(async (results) => { let aliases = this.getAliasesFromPassthroughResults(results); await this.config.events.emit("eleventy.passthrough", { map: aliases, }); debug(`TemplatePassthrough copy finished. Current count: ${this.count} (size: ${this.size})`); return results; }); } } export default TemplatePassthroughManager; ================================================ FILE: src/TemplatePermalink.js ================================================ import path from "node:path"; import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; class TemplatePermalink { #dynamicPermalinkEnabled; // `link` with template syntax should have already been rendered in Template.js constructor(link, extraSubdir, isDynamicPermalinkEnabled = true) { let isLinkAnObject = isPlainObject(link); this._isRendered = true; this._writeToFileSystem = true; this.#dynamicPermalinkEnabled = isDynamicPermalinkEnabled; let buildLink; if (isLinkAnObject) { if ("build" in link) { buildLink = link.build; } // find the first string key for (let key in link) { if (typeof key !== "string") { continue; } break; } } else { buildLink = link; } // permalink: false and permalink: build: false if (typeof buildLink === "boolean") { if (buildLink === false) { this._writeToFileSystem = false; } else { throw new Error( `\`permalink: ${ isLinkAnObject ? "build: " : "" }true\` is not a supported feature in Eleventy. Did you mean \`permalink: ${ isLinkAnObject ? "build: " : "" }false\`?`, ); } } else if (buildLink) { if (typeof buildLink !== "string") { let stringToString = "toString" in buildLink ? `:\n\n${buildLink.toString()}` : ""; throw new Error( `Expected permalink value to be a string. Received \`${typeof buildLink}\` (dynamicPermalink: ${this.#dynamicPermalinkEnabled})${stringToString}`, ); } this.buildLink = buildLink; } if (isLinkAnObject) { // default if permalink is an Object but does not have a `build` prop if (!("build" in link)) { this._writeToFileSystem = false; this._isRendered = false; } } this.extraPaginationSubdir = extraSubdir || ""; } setUrlTransforms(transforms) { this._urlTransforms = transforms; } get urlTransforms() { return this._urlTransforms || []; } _addDefaultLinkFilename(link) { return link + (link.slice(-1) === "/" ? "index.html" : ""); } toOutputPath() { if (!this.buildLink) { // empty or false return false; } let cleanLink = this._addDefaultLinkFilename(this.buildLink); let parsed = path.parse(cleanLink); return TemplatePath.join(parsed.dir, this.extraPaginationSubdir, parsed.base); } // Used in url transforms feature static getUrlStem(original) { let subject = original; if (original.endsWith(".html")) { subject = original.slice(0, -1 * ".html".length); } return TemplatePermalink.normalizePathToUrl(subject); } static normalizePathToUrl(original) { let compare = original || ""; let needleHtml = "/index.html"; let needleBareTrailingSlash = "/index/"; let needleBare = "/index"; if (compare.endsWith(needleHtml)) { return compare.slice(0, compare.length - needleHtml.length) + "/"; } else if (compare.endsWith(needleBareTrailingSlash)) { return compare.slice(0, compare.length - needleBareTrailingSlash.length) + "/"; } else if (compare.endsWith(needleBare)) { return compare.slice(0, compare.length - needleBare.length) + "/"; } return original; } // This method is used to generate the `page.url` variable. // remove all index.html’s from links // index.html becomes / // test/index.html becomes test/ toHref() { if (!this.buildLink) { // empty or false return false; } let transformedLink = this.toOutputPath(); let original = (transformedLink.charAt(0) !== "/" ? "/" : "") + transformedLink; let normalized = TemplatePermalink.normalizePathToUrl(original) || ""; for (let transform of this.urlTransforms) { original = transform({ url: normalized, urlStem: TemplatePermalink.getUrlStem(original), }) ?? original; } return TemplatePermalink.normalizePathToUrl(original); } toPath(outputDir) { if (!this.buildLink) { return false; } let uri = this.toOutputPath(); if (uri === false) { return false; } return TemplatePath.addLeadingDotSlash(TemplatePath.normalize(outputDir + "/" + uri)); } toPathFromRoot() { if (!this.buildLink) { return false; } let uri = this.toOutputPath(); if (uri === false) { return false; } return TemplatePath.addLeadingDotSlash(TemplatePath.normalize(uri)); } static _hasDuplicateFolder(dir, base) { let folders = dir.split("/"); if (!folders[folders.length - 1]) { folders.pop(); } return folders[folders.length - 1] === base; } static generate( dir, filenameNoExt, extraSubdir, fileExtension = "html", isDynamicPermalinkEnabled, ) { let path; if (fileExtension === "html") { let hasDupeFolder = TemplatePermalink._hasDuplicateFolder(dir, filenameNoExt); path = (dir ? dir + "/" : "") + (filenameNoExt !== "index" && !hasDupeFolder ? filenameNoExt + "/" : "") + "index.html"; } else { path = (dir ? dir + "/" : "") + filenameNoExt + "." + fileExtension; } return new TemplatePermalink(path, extraSubdir, isDynamicPermalinkEnabled); } } export default TemplatePermalink; ================================================ FILE: src/TemplatePreprocessors.js ================================================ export class TemplatePreprocessors { constructor(preprocessors) { this.preprocessors = preprocessors || []; } async runAll(template, data) { let { inputPath } = template; let content = await template.getPreRender(); let skippedVia = false; for (let [name, preprocessor] of Object.entries(this.preprocessors)) { let { filter, callback } = preprocessor; let filters; if (Array.isArray(filter)) { filters = filter; } else if (typeof filter === "string") { filters = filter.split(","); } else { throw new Error( `Expected file extensions passed to "${name}" content preprocessor to be a string or array. Received: ${filter}`, ); } filters = filters.map((extension) => { if (extension.startsWith(".") || extension === "*") { return extension; } return `.${extension}`; }); if (!filters.some((extension) => extension === "*" || inputPath.endsWith(extension))) { // skip continue; } try { let ret = await callback.call( { inputPath, }, data, content, ); // Returning explicit false is the same as ignoring the template if (ret === false) { skippedVia = name; continue; } // Different from transforms: returning falsy (not false) here does nothing (skips the preprocessor) if (ret) { content = ret; } } catch (e) { throw new Error( `Preprocessor \`${name}\` encountered an error when transforming ${inputPath}.`, { cause: e }, ); } } return { skippedVia, content, }; } } ================================================ FILE: src/TemplateRender.js ================================================ import debugUtil from "debug"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import TemplateEngineManager from "./Engines/TemplateEngineManager.js"; const debugConfiguration = debugUtil("Eleventy:UserConfig"); class TemplateRenderUnknownEngineError extends EleventyBaseError {} // works with full path names or short engine name export default class TemplateRender { #extensionMap; #config; constructor(tmplPath, config) { if (!tmplPath) { throw new Error(`TemplateRender requires a tmplPath argument, instead of ${tmplPath}`); } this.#setConfig(config); this.engineNameOrPath = tmplPath; this.parseMarkdownWith = this.config.markdownTemplateEngine; if (this.parseMarkdownWith === "md") { this.parseMarkdownWith = false; debugConfiguration( "Misconfiguration warning: the preprocessing template syntax for Markdown files cannot be Markdown, we’re assuming you meant `false` to skip preprocessing altogether (via the `markdownTemplateEngine` configuration property or the `setMarkdownTemplateEngine` configuration method). Read more: https://www.11ty.dev/docs/config/#default-template-engine-for-markdown-files", ); } this.parseHtmlWith = this.config.htmlTemplateEngine; } #setConfig(config) { if (config?.constructor?.name !== "TemplateConfig") { throw new Error("TemplateRender must receive a TemplateConfig instance."); } this.eleventyConfig = config; this.config = config.getConfig(); } get dirs() { return this.eleventyConfig.directories; } get inputDir() { return this.dirs.input; } get includesDir() { return this.dirs.includes; } /* Backwards compat */ getIncludesDir() { return this.includesDir; } get config() { return this.#config; } set config(config) { this.#config = config; } set extensionMap(extensionMap) { this.#extensionMap = extensionMap; } get extensionMap() { if (!this.#extensionMap) { throw new Error("Internal error: missing `extensionMap` in TemplateRender."); } return this.#extensionMap; } async getEngineByName(name) { // WARNING: eleventyConfig assignment removed here return this.extensionMap.engineManager.getEngine(name, this.extensionMap); } // Runs once per template async init(engineNameOrPath) { let name = engineNameOrPath || this.engineNameOrPath; this.extensionMap.setTemplateConfig(this.eleventyConfig); let extensionEntry = this.extensionMap.getExtensionEntry(name); let engineName = extensionEntry?.aliasKey || extensionEntry?.key; if (TemplateEngineManager.isSimpleAlias(extensionEntry)) { engineName = extensionEntry?.key; } this._engineName = engineName; if (!extensionEntry || !this._engineName) { throw new TemplateRenderUnknownEngineError( `Unknown engine for ${name} (supported extensions: ${this.extensionMap.getReadableFileExtensions()})`, ); } this._engine = await this.getEngineByName(this._engineName); if (this.useMarkdown === undefined) { this.setUseMarkdown(this._engineName === "md"); } } get engineName() { if (!this._engineName) { throw new Error("TemplateRender needs a call to the init() method."); } return this._engineName; } get engine() { if (!this._engine) { throw new Error("TemplateRender needs a call to the init() method."); } return this._engine; } static parseEngineOverrides(engineName) { if (typeof (engineName || "") !== "string") { throw new Error("Expected String passed to parseEngineOverrides. Received: " + engineName); } let overlappingEngineWarningCount = 0; let engines = []; let uniqueLookup = {}; let usingMarkdown = false; (engineName || "") .split(",") .map((name) => { return name.toLowerCase().trim(); }) .forEach((name) => { // html is assumed (treated as plaintext by the system) if (!name || name === "html") { return; } if (name === "md") { usingMarkdown = true; return; } if (!uniqueLookup[name]) { engines.push(name); uniqueLookup[name] = true; // we already short circuit md and html types above overlappingEngineWarningCount++; } }); if (overlappingEngineWarningCount > 1) { throw new Error( `Don’t mix multiple templating engines in your front matter overrides (exceptions for HTML and Markdown). You used: ${engineName}`, ); } // markdown should always be first if (usingMarkdown) { engines.unshift("md"); } return engines; } // used for error logging and console output. getReadableEnginesList() { return this.getReadableEnginesListDifferingFromFileExtension() || this.engineName; } getReadableEnginesListDifferingFromFileExtension() { let keyFromFilename = this.extensionMap.getKey(this.engineNameOrPath); if (this.engine?.constructor?.name === "CustomEngine") { if ( this.engine.entry && this.engine.entry.name && keyFromFilename !== this.engine.entry.name ) { return this.engine.entry.name; } else { // We don’t have a name for it so we return nothing so we don’t misreport (per #2386) return; } } if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) { return this.parseMarkdownWith; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } // templateEngineOverride in play and template language differs from file extension if (keyFromFilename !== this.engineName) { return this.engineName; } } // TODO templateEngineOverride getPreprocessorEngineName() { if (this.engineName === "md" && this.parseMarkdownWith) { return this.parseMarkdownWith; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } // TODO do we need this? return this.extensionMap.getKey(this.engineNameOrPath); } // We pass in templateEngineOverride here because it isn’t yet applied to templateRender getEnginesList(engineOverride) { if (engineOverride) { let engines = TemplateRender.parseEngineOverrides(engineOverride).reverse(); return engines.join(","); } if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) { return `${this.parseMarkdownWith},md`; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } // templateEngineOverride in play return this.extensionMap.getKey(this.engineNameOrPath); } async setEngineOverride(engineName, bypassMarkdown) { let engines = TemplateRender.parseEngineOverrides(engineName); // when overriding, Template Engines with HTML will instead use the Template Engine as primary and output HTML // So any HTML engine usage here will never use a preprocessor templating engine. this.setHtmlEngine(false); if (!engines.length) { await this.init("html"); return; } await this.init(engines[0]); let usingMarkdown = engines[0] === "md" && !bypassMarkdown; this.setUseMarkdown(usingMarkdown); if (usingMarkdown) { // false means only parse markdown and not with a preprocessor template engine this.setMarkdownEngine(engines.length > 1 ? engines[1] : false); } } getEngineName() { return this.engineName; } isEngine(engine) { return this.engineName === engine; } setUseMarkdown(useMarkdown) { this.useMarkdown = !!useMarkdown; } // this is only called for templateEngineOverride setMarkdownEngine(markdownEngine) { this.parseMarkdownWith = markdownEngine; } // this is only called for templateEngineOverride setHtmlEngine(htmlEngineName) { this.parseHtmlWith = htmlEngineName; } async _testRender(str, data) { return this.engine._testRender(str, data); } async getCompiledTemplate(str) { // TODO refactor better, move into TemplateEngine logic if (this.engineName === "md") { return this.engine.compile( str, this.engineNameOrPath, this.parseMarkdownWith, !this.useMarkdown, ); } else if (this.engineName === "html") { return this.engine.compile(str, this.engineNameOrPath, this.parseHtmlWith); } else { return this.engine.compile(str, this.engineNameOrPath); } } } ================================================ FILE: src/TemplateWriter.js ================================================ import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; import Template from "./Template.js"; import TemplateMap from "./TemplateMap.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import { EleventyErrorHandler } from "./Errors/EleventyErrorHandler.js"; import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js"; import ConsoleLogger from "./Util/ConsoleLogger.js"; const debug = debugUtil("Eleventy:TemplateWriter"); class TemplateWriterMissingConfigArgError extends EleventyBaseError {} class EleventyPassthroughCopyError extends EleventyBaseError {} class EleventyTemplateError extends EleventyBaseError {} class TemplateWriter { #eleventyFiles; #passthroughManager; #errorHandler; #extensionMap; constructor( templateFormats, // TODO remove this in favor of this.#eleventyFiles templateData, templateConfig, ) { if (!templateConfig) { throw new TemplateWriterMissingConfigArgError("Missing config argument."); } this.templateConfig = templateConfig; this.config = templateConfig.getConfig(); this.userConfig = templateConfig.userConfig; this.templateFormats = templateFormats; this.templateData = templateData; this.isVerbose = true; this.isDryRun = false; this.writeCount = 0; this.renderCount = 0; this.skippedCount = 0; this.isRunInitialBuild = true; this._templatePathCache = new Map(); } get dirs() { return this.templateConfig.directories; } get inputDir() { return this.dirs.input; } get outputDir() { return this.dirs.output; } get templateFormats() { return this._templateFormats; } set templateFormats(value) { this._templateFormats = value; } /* Getter for error handler */ get errorHandler() { if (!this.#errorHandler) { this.#errorHandler = new EleventyErrorHandler(); this.#errorHandler.isVerbose = this.verboseMode; this.#errorHandler.logger = this.logger; } return this.#errorHandler; } /* Getter for Logger */ get logger() { if (!this._logger) { this._logger = new ConsoleLogger(); this._logger.isVerbose = this.verboseMode; } return this._logger; } /* Setter for Logger */ set logger(logger) { this._logger = logger; } /* For testing */ overrideConfig(config) { this.config = config; } restart() { this.writeCount = 0; this.renderCount = 0; this.skippedCount = 0; } set extensionMap(extensionMap) { this.#extensionMap = extensionMap; } get extensionMap() { if (!this.#extensionMap) { throw new Error("Internal error: missing `extensionMap` in TemplateWriter."); } return this.#extensionMap; } setPassthroughManager(mgr) { this.#passthroughManager = mgr; } setEleventyFiles(eleventyFiles) { this.#eleventyFiles = eleventyFiles; } // Tests getPassthroughGlobs() { return this.#eleventyFiles?.passthroughGlobs; } getPathsWithVirtualTemplates(paths) { // Support for virtual templates added in 3.0 if (this.config.virtualTemplates && isPlainObject(this.config.virtualTemplates)) { let virtualTemplates = Object.keys(this.config.virtualTemplates) .filter((path) => { // Filter out includes/layouts return this.dirs.isTemplateFile(path); }) .map((path) => { let fullVirtualPath = this.dirs.getInputPath(path); if (!this.extensionMap.getKey(fullVirtualPath)) { throw new Error( `The virtual template at ${fullVirtualPath} is using a template format that’s not valid for your project. Your project is using: "${this.formats}". Read more about formats: https://v3.11ty.dev/docs/config/#template-formats`, ); } return fullVirtualPath; }); paths = paths.concat(virtualTemplates); // Virtual templates can not live at the same place as files on the file system! if (paths.length !== new Set(paths).size) { let conflicts = {}; for (let path of paths) { if (conflicts[path]) { throw new Error( `A virtual template had the same path as a file on the file system: "${path}"`, ); } conflicts[path] = true; } } } return paths; } async _getAllPaths() { if (!this.#eleventyFiles) { return this.getPathsWithVirtualTemplates([]); } // this is now cached upstream by FileSystemSearch let paths = await this.#eleventyFiles.getFiles(); paths = this.getPathsWithVirtualTemplates(paths); return paths; } _createTemplate(path, to = "fs") { let tmpl = this._templatePathCache.get(path); let wasCached = false; if (tmpl) { wasCached = true; // Update config for https://github.com/11ty/eleventy/issues/3468 // TODO reset other constructor things here like inputDir/outputDir tmpl.resetCachedTemplate({ templateData: this.templateData, extensionMap: this.extensionMap, eleventyConfig: this.templateConfig, }); } else { tmpl = new Template(path, this.templateData, this.extensionMap, this.templateConfig); tmpl.setOutputFormat(to); tmpl.logger = this.logger; this._templatePathCache.set(path, tmpl); } tmpl.setTransforms(this.config.transforms); tmpl.setLinters(this.config.linters); tmpl.setDryRun(this.isDryRun); tmpl.setIsVerbose(this.isVerbose); tmpl.reset(); return { template: tmpl, wasCached, }; } // incrementalFileShape is `template` or `copy` (for passthrough file copy) async _addToTemplateMapIncrementalBuild(incrementalFileShape, paths, to = "fs") { // Render overrides are only used when `--ignore-initial` is in play and an initial build is not run let ignoreInitialBuild = !this.isRunInitialBuild; let secondOrderRelevantLookup = {}; let templates = []; let promises = []; for (let path of paths) { let { template: tmpl } = this._createTemplate(path, to); // Note: removed a fix here to fetch missing templateRender instances // that was tested as no longer needed (Issue #3170) // Related: #3870, improved configuration reset templates.push(tmpl); // required for tmpl.isFileRelevantToThisTemplate below await tmpl.asyncTemplateInitialization(); // This must happen before data is generated for the incremental file only if (incrementalFileShape === "template" && tmpl.inputPath === this.incrementalFile) { tmpl.resetCaches(); } else if ( // Issue #3824 #3870 tmpl.isFileRelevantToThisTemplate(this.incrementalFile, { isFullTemplate: incrementalFileShape === "template", }) ) { tmpl.resetCaches(); } // IMPORTANT: This is where the data is first generated for the template promises.push(this.templateMap.add(tmpl)); } // Important to set up template dependency relationships first await Promise.all(promises); // Delete incremental file from the dependency graph so we get fresh entries! // This _must_ happen before any additions, the other ones are in Custom.js and GlobalDependencyMap.js (from the eleventy.layouts Event) this.config.uses.resetNode(this.incrementalFile); // write new template relationships to the global dependency graph for next time this.templateMap.addAllToGlobalDependencyGraph(); // Always disable render for --ignore-initial if (ignoreInitialBuild) { for (let tmpl of templates) { tmpl.setRenderableOverride(false); // disable render } return; } for (let tmpl of templates) { if (incrementalFileShape === "template" && tmpl.inputPath === this.incrementalFile) { tmpl.setRenderableOverride(undefined); // unset, probably render } else if ( tmpl.isFileRelevantToThisTemplate(this.incrementalFile, { isFullTemplate: incrementalFileShape === "template", }) ) { // changed file is used by template // template uses the changed file tmpl.setRenderableOverride(undefined); // unset, probably render secondOrderRelevantLookup[tmpl.inputPath] = true; } else if (this.config.uses.isFileUsedBy(this.incrementalFile, tmpl.inputPath)) { // changed file uses this template tmpl.setRenderableOverride("optional"); } else { // For incremental, always disable render on irrelevant templates tmpl.setRenderableOverride(false); // disable render } } let secondOrderRelevantArray = this.config.uses .getTemplatesRelevantToTemplateList(Object.keys(secondOrderRelevantLookup)) .map((entry) => TemplatePath.addLeadingDotSlash(entry)); let secondOrderTemplates = Object.fromEntries( Object.entries(secondOrderRelevantArray).map(([index, value]) => [value, true]), ); for (let tmpl of templates) { // second order templates must also be rendered if not yet already rendered at least once and available in cache. if (secondOrderTemplates[tmpl.inputPath]) { if (tmpl.isRenderableDisabled()) { tmpl.setRenderableOverride("optional"); } } } // Order of templates does not matter here, they’re reordered later based on dependencies in TemplateMap.js for (let tmpl of templates) { if (incrementalFileShape === "template" && tmpl.inputPath === this.incrementalFile) { // Cache is reset above (to invalidate data cache at the right time) tmpl.setDryRunViaIncremental(false); } else if (!tmpl.isRenderableDisabled() && !tmpl.isRenderableOptional()) { // Related to the template but not the template (reset the render cache, not the read cache) tmpl.resetCaches({ data: true, render: true, }); tmpl.setDryRunViaIncremental(false); } else { // During incremental we only reset the data cache for non-matching templates, see https://github.com/11ty/eleventy/issues/2710 // Keep caches for read/render tmpl.resetCaches({ data: true, }); tmpl.setDryRunViaIncremental(true); this.skippedCount++; } } } async _addToTemplateMapFullBuild(paths, to = "fs") { if (this.incrementalFile) { return []; } let ignoreInitialBuild = !this.isRunInitialBuild; let promises = []; for (let path of paths) { let { template: tmpl, wasCached } = this._createTemplate(path, to); // Render overrides are only used when `--ignore-initial` is in play and an initial build is not run if (ignoreInitialBuild) { tmpl.setRenderableOverride(false); // disable render } else { tmpl.setRenderableOverride(undefined); // unset, render } if (wasCached) { tmpl.resetCaches(); } // IMPORTANT: This is where the data is first generated for the template promises.push(this.templateMap.add(tmpl)); } return Promise.all(promises); } getFileShape(paths, incrementalFile) { // WARNING: This is leaky—if Core is being used instead of Eleventy we are assuming everything is a template (not passthrough copy) if (!this.#eleventyFiles) { return "template"; } return this.#eleventyFiles.getFileShape(paths, incrementalFile); } async _addToTemplateMap(paths, to = "fs") { let incrementalFileShape = this.getFileShape(paths, this.incrementalFile); // Filter out passthrough copy files paths = paths.filter((path) => { if (!this.extensionMap.hasEngine(path)) { return false; } if (incrementalFileShape === "copy") { this.skippedCount++; // Filters out templates if the incremental file is a passthrough copy file return false; } return true; }); if (this.incrementalFile) { // Top level async to get at the promises returned. return await this._addToTemplateMapIncrementalBuild(incrementalFileShape, paths, to); } // Full Build let ret = await this._addToTemplateMapFullBuild(paths, to); // write new template relationships to the global dependency graph for next time this.templateMap.addAllToGlobalDependencyGraph(); return ret; } async _createTemplateMap(paths, to) { this.templateMap = new TemplateMap(this.templateConfig); await this._addToTemplateMap(paths, to); await this.templateMap.cache(); // Return is used by tests return this.templateMap; } async _generateTemplate(mapEntry, to) { let tmpl = mapEntry.template; return tmpl.generateMapEntry(mapEntry, to).then((pages) => { this.renderCount += tmpl.getRenderCount(); this.writeCount += tmpl.getWriteCount(); return pages; }); } async writePassthroughCopy(templateExtensionPaths) { if (!this.#passthroughManager) { throw new Error("Internal error: Missing `passthroughManager` instance."); } return this.#passthroughManager.copyAll(templateExtensionPaths).catch((e) => { this.errorHandler.warn(e, "Error with passthrough copy"); return Promise.reject(new EleventyPassthroughCopyError("Having trouble copying", e)); }); } async generateTemplates(paths, to = "fs") { let promises = []; // TODO optimize await here await this._createTemplateMap(paths, to); debug("Template map created."); let usedTemplateContentTooEarlyMap = []; for (let mapEntry of this.templateMap.getMap()) { promises.push( this._generateTemplate(mapEntry, to).catch(function (e) { // Premature templateContent in layout render, this also happens in // TemplateMap.populateContentDataInMap for non-layout content if (EleventyErrorUtil.isPrematureTemplateContentError(e)) { usedTemplateContentTooEarlyMap.push(mapEntry); } else { let outputPaths = `"${mapEntry._pages.map((page) => page.outputPath).join(`", "`)}"`; return Promise.reject( new EleventyTemplateError( `Having trouble writing to ${outputPaths} from "${mapEntry.inputPath}"`, e, ), ); } }), ); } for (let mapEntry of usedTemplateContentTooEarlyMap) { promises.push( this._generateTemplate(mapEntry, to).catch(function (e) { return Promise.reject( new EleventyTemplateError( `Having trouble writing to (second pass) "${mapEntry.outputPath}" from "${mapEntry.inputPath}"`, e, ), ); }), ); } return promises; } // Similiar to `write()` but skips passthrough copy async writeTemplates() { let paths = await this._getAllPaths(); return Promise.all(await this.generateTemplates(paths)).then( (templateResults) => { return { // New in 3.0: flatten and filter out falsy templates templates: templateResults.flat().filter(Boolean), }; }, (e) => { return Promise.reject(e); }, ); } async write() { let paths = await this._getAllPaths(); // This must happen before writePassthroughCopy this.templateConfig.userConfig.emit("eleventy#beforerender"); let aggregatePassthroughCopyPromise = this.writePassthroughCopy(paths); let templatesPromise = Promise.all(await this.generateTemplates(paths)).then((results) => { this.templateConfig.userConfig.emit("eleventy#render"); return results; }); return Promise.all([aggregatePassthroughCopyPromise, templatesPromise]).then( async ([passthroughCopyResults, templateResults]) => { return { passthroughCopy: passthroughCopyResults, // New in 3.0: flatten and filter out falsy templates templates: templateResults.flat().filter(Boolean), }; }, (e) => { return Promise.reject(e); }, ); } // Passthrough copy not supported in JSON output. // --incremental not supported in JSON output. async getJSON(to = "json") { let paths = await this._getAllPaths(); let promises = await this.generateTemplates(paths, to); return Promise.all(promises).then( (templateResults) => { return { // New in 3.0: flatten and filter out falsy templates templates: templateResults.flat().filter(Boolean), }; }, (e) => { return Promise.reject(e); }, ); } setVerboseOutput(isVerbose) { this.isVerbose = isVerbose; this.errorHandler.isVerbose = isVerbose; } setDryRun(isDryRun) { this.isDryRun = Boolean(isDryRun); } setRunInitialBuild(runInitialBuild) { this.isRunInitialBuild = runInitialBuild; } setIncrementalBuild(isIncremental) { this.isIncremental = isIncremental; } setIncrementalFile(incrementalFile) { this.incrementalFile = incrementalFile; this.#passthroughManager?.setIncrementalFile(incrementalFile); } resetIncrementalFile() { this.incrementalFile = null; this.#passthroughManager?.resetIncrementalFile(); } getMetadata() { return { // copyCount, copySize ...(this.#passthroughManager?.getMetadata() || {}), skipCount: this.skippedCount, writeCount: this.writeCount, renderCount: this.renderCount, }; } get caches() { return ["_templatePathCache"]; } } export default TemplateWriter; ================================================ FILE: src/UserConfig.js ================================================ import debugUtil from "debug"; import { DeepCopy, TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import chalk from "./Adapters/Packages/chalk.js"; import { resolvePlugin } from "./Util/ResolvePlugin.js"; import isAsyncFunction from "./Util/IsAsyncFunction.js"; import objectFilter from "./Util/Objects/ObjectFilter.js"; import EventEmitter from "./Util/AsyncEventEmitter.js"; import EleventyCompatibility from "./Util/Compatibility.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import BenchmarkManager from "./Benchmark/BenchmarkManager.js"; import { augmentFunction } from "./Engines/Util/ContextAugmenter.js"; const debug = debugUtil("Eleventy:UserConfig"); class UserConfigError extends EleventyBaseError {} /** * Eleventy’s user-land Configuration API * @module 11ty/eleventy/UserConfig */ class UserConfig { /** @type {boolean} */ #pluginExecution = false; /** @type {boolean} */ #quietModeLocked = false; /** @type {number|undefined} */ #uniqueId; /** @type {number} */ #concurrency = 1; // Before using os.availableParallelism(); see https://github.com/11ty/eleventy/issues/3596 constructor() { // These are completely unnecessary lines to satisfy TypeScript this.plugins = []; this.templateFormatsAdded = []; this.additionalWatchTargets = []; this.watchTargetsConfigReset = new Set(); this.extensionMap = new Set(); this.extensionMapClasses = {}; this.dataExtensions = new Map(); this.urlTransforms = []; this.customDateParsingCallbacks = new Set(); this.ignores = new Set(); this.events = new EventEmitter(); /** @type {object} */ this.directories = {}; /** @type {undefined} */ this.logger; /** @type {string} */ this.dir; /** @type {string} */ this.pathPrefix; /** @type {object} */ this.errorReporting = {}; this.reset(); this.#uniqueId = Math.random(); } // Internally used in TemplateContent for cache keys _getUniqueId() { return this.#uniqueId; } reset() { debug("Resetting EleventyConfig to initial values."); /** @type {EventEmitter} */ this.events = new EventEmitter(); this.events.setMaxListeners(25); // defaults to 10 /** @type {BenchmarkManager} */ this.benchmarkManager = new BenchmarkManager(); /** @type {object} */ this.benchmarks = { /** @type {import('./Benchmark/BenchmarkGroup.js')} */ config: this.benchmarkManager.get("Configuration"), /** @type {import('./Benchmark/BenchmarkGroup.js')} */ aggregate: this.benchmarkManager.get("Aggregate"), }; /** @type {object} */ this.directoryAssignments = {}; /** @type {object} */ this.collections = {}; /** @type {object} */ this.precompiledCollections = {}; this.templateFormats = undefined; this.templateFormatsAdded = []; /** @type {object} */ this.universal = { filters: {}, shortcodes: {}, pairedShortcodes: {}, }; /** @type {object} */ this.liquid = { options: {}, tags: {}, filters: {}, shortcodes: {}, pairedShortcodes: {}, parameterParsing: "builtin", // or legacy (Breaking: default swapped in v4.0.0) }; /** @type {object} */ this.nunjucks = { // `dev: true` gives us better error messaging environmentOptions: { dev: true }, precompiledTemplates: {}, loaders: [], filters: {}, asyncFilters: {}, tags: {}, globals: {}, shortcodes: {}, pairedShortcodes: {}, asyncShortcodes: {}, asyncPairedShortcodes: {}, }; /** @type {object} */ this.javascript = { functions: {}, filters: {}, shortcodes: {}, pairedShortcodes: {}, }; this.markdownHighlighter = null; /** @type {object} */ this.libraryOverrides = {}; /** @type {object} */ this.passthroughCopies = {}; this.passthroughCopiesHtmlRelative = new Set(); /** @type {object} */ this.layoutAliases = {}; this.layoutResolution = true; // extension-less layout files /** @type {object} */ this.linters = {}; /** @type {object} */ this.transforms = {}; /** @type {object} */ this.preprocessors = {}; this.activeNamespace = ""; this.dynamicPermalinks = true; this.useGitIgnore = true; let defaultIgnores = new Set(); defaultIgnores.add("**/node_modules/**"); defaultIgnores.add(".git/**"); // TODO `**/.git/**` this.ignores = new Set(defaultIgnores); this.watchIgnores = new Set(defaultIgnores); this.extensionMap = new Set(); this.extensionMapClasses = {}; /** @type {object} */ this.extensionConflictMap = {}; this.watchJavaScriptDependencies = true; this.additionalWatchTargets = []; this.watchTargetsConfigReset = new Set(); /** @type {object} */ this.serverOptions = {}; /** @type {object} */ this.globalData = {}; /** @type {object} */ this.chokidarConfig = {}; this.watchThrottleWaitTime = 0; //ms // using Map to preserve insertion order this.dataExtensions = new Map(); this.quietMode = false; this.plugins = []; this.useTemplateCache = true; this.dataFilterSelectors = new Set(); /** @type {object} */ this.libraryAmendments = {}; this.serverPassthroughCopyBehavior = "copy"; // or "passthrough" this.urlTransforms = []; // Defaults in `defaultConfig.js` this.dataFileSuffixesOverride = false; this.dataFileDirBaseNameOverride = false; /** @type {object} */ // Moved into TemplateContent->getFrontMatterParsingOptions this.frontMatterParsingOptions = {}; /** @type {object} */ this.virtualTemplates = {}; this.freezeReservedData = true; this.customDateParsingCallbacks = new Set(); /** @type {object} */ this.errorReporting = {}; // Before using os.availableParallelism(); see https://github.com/11ty/eleventy/issues/3596 this.#concurrency = 1; } // compatibleRange is optional in 2.0.0-beta.2 versionCheck(compatibleRange) { let compat = new EleventyCompatibility(compatibleRange); if (!compat.isCompatible()) { throw new UserConfigError(compat.getErrorMessage()); } } /* * Events */ // Duplicate event bindings are avoided with the `reset` method above. // A new EventEmitter instance is created when the config is reset. on(eventName, callback) { return this.events.on(eventName, callback); } once(eventName, callback) { return this.events.once(eventName, callback); } emit(eventName, ...args) { return this.events.emit(eventName, ...args); } setEventEmitterMode(mode) { this.events.setHandlerMode(mode); } /* * Universal getters */ getFilter(name) { // JavaScript functions are included here for backwards compatibility https://github.com/11ty/eleventy/issues/3365 return this.universal.filters[name] || this.javascript.functions[name]; } getFilters(options = {}) { if (options.type) { return objectFilter( this.universal.filters, (entry) => entry.__eleventyInternal?.type === options.type, ); } return this.universal.filters; } getShortcode(name) { return this.universal.shortcodes[name]; } getShortcodes(options = {}) { if (options.type) { return objectFilter( this.universal.shortcodes, (entry) => entry.__eleventyInternal?.type === options.type, ); } return this.universal.shortcodes; } getPairedShortcode(name) { return this.universal.pairedShortcodes[name]; } getPairedShortcodes(options = {}) { if (options.type) { return objectFilter( this.universal.pairedShortcodes, (entry) => entry.__eleventyInternal?.type === options.type, ); } return this.universal.pairedShortcodes; } /* * Private utilities */ #add(target, originalName, callback, options) { let { description, functionName } = options; if (typeof callback !== "function") { throw new Error(`Invalid definition for "${originalName}" ${description}.`); } let name = this.getNamespacedName(originalName); if (target[name]) { debug( chalk.yellow(`Warning, overwriting previous ${description} "%o" via \`%o(%o)\``), name, functionName, originalName, ); } else { debug(`Adding new ${description} "%o" via \`%o(%o)\``, name, functionName, originalName); } target[name] = this.#decorateCallback(`"${name}" ${description}`, callback); } #decorateCallback(type, callback) { return this.benchmarks.config.add(type, callback); } /* * Markdown */ // Don’t use this, projects should use `amendLibrary` as documented here: // https://www.11ty.dev/docs/languages/markdown/#optional-amend-the-library-instance // Warning: this is in use by the Syntax Highlighting plugin (as of v5.0.2) addMarkdownHighlighter(highlightFn) { this.markdownHighlighter = highlightFn; } setMarkdownTemplateEngine(engineName) { this.markdownTemplateEngine = engineName; } setHtmlTemplateEngine(engineName) { this.htmlTemplateEngine = engineName; } /* * Filters */ addLiquidFilter(name, callback) { this.#add(this.liquid.filters, name, callback, { description: "Liquid Filter", functionName: "addLiquidFilter", }); } addNunjucksAsyncFilter(name, callback) { this.#add(this.nunjucks.asyncFilters, name, callback, { description: "Nunjucks Filter", functionName: "addNunjucksAsyncFilter", }); } // Support the nunjucks style syntax for asynchronous filter add addNunjucksFilter(name, callback, isAsync = false) { if (isAsync) { // namespacing happens downstream this.addNunjucksAsyncFilter(name, callback); } else { this.#add(this.nunjucks.filters, name, callback, { description: "Nunjucks Filter", functionName: "addNunjucksFilter", }); } } addJavaScriptFilter(name, callback) { this.#add(this.javascript.filters, name, callback, { description: "JavaScript Filter", functionName: "addJavaScriptFilter", }); // Backwards compat for a time before `addJavaScriptFilter` existed. this.addJavaScriptFunction(name, callback); } addFilter(name, callback) { // This method *requires* `async function` and will not work with `function` that returns a promise if (isAsyncFunction(callback)) { this.addAsyncFilter(name, callback); return; } // namespacing happens downstream this.#add(this.universal.filters, name, callback, { description: "Universal Filter", functionName: "addFilter", }); this.addLiquidFilter(name, callback); this.addJavaScriptFilter(name, callback); this.addNunjucksFilter( name, /** @this {any} */ function (...args) { // Note that `callback` is already a function as the `#add` method throws an error if not. let ret = callback.call(this, ...args); if (ret instanceof Promise) { throw new Error( `Nunjucks *is* async-friendly with \`addFilter("${name}", async function() {})\` but you need to supply an \`async function\`. You returned a promise from \`addFilter("${name}", function() {})\`. Alternatively, use the \`addAsyncFilter("${name}")\` configuration API method.`, ); } return ret; }, ); } // Liquid, Nunjucks, and JS only addAsyncFilter(name, callback) { // namespacing happens downstream this.#add(this.universal.filters, name, callback, { description: "Universal Filter", functionName: "addAsyncFilter", }); this.addLiquidFilter(name, callback); this.addJavaScriptFilter(name, callback); this.addNunjucksAsyncFilter( name, /** @this {any} */ async function (...args) { let cb = args.pop(); // Note that `callback` is already a function as the `#add` method throws an error if not. let ret = await callback.call(this, ...args); cb(null, ret); }, ); } /* * Shortcodes */ addShortcode(name, callback) { // This method *requires* `async function` and will not work with `function` that returns a promise if (isAsyncFunction(callback)) { this.addAsyncShortcode(name, callback); return; } this.#add(this.universal.shortcodes, name, callback, { description: "Universal Shortcode", functionName: "addShortcode", }); this.addLiquidShortcode(name, callback); this.addJavaScriptShortcode(name, callback); this.addNunjucksShortcode(name, callback); } addAsyncShortcode(name, callback) { this.#add(this.universal.shortcodes, name, callback, { description: "Universal Shortcode", functionName: "addAsyncShortcode", }); // Related: #498 this.addNunjucksAsyncShortcode(name, callback); this.addLiquidShortcode(name, callback); this.addJavaScriptShortcode(name, callback); } addNunjucksAsyncShortcode(name, callback) { this.#add(this.nunjucks.asyncShortcodes, name, callback, { description: "Nunjucks Async Shortcode", functionName: "addNunjucksAsyncShortcode", }); } addNunjucksShortcode(name, callback, isAsync = false) { if (isAsync) { this.addNunjucksAsyncShortcode(name, callback); } else { this.#add(this.nunjucks.shortcodes, name, callback, { description: "Nunjucks Shortcode", functionName: "addNunjucksShortcode", }); } } addLiquidShortcode(name, callback) { this.#add(this.liquid.shortcodes, name, callback, { description: "Liquid Shortcode", functionName: "addLiquidShortcode", }); } addPairedShortcode(name, callback) { // This method *requires* `async function` and will not work with `function` that returns a promise if (isAsyncFunction(callback)) { this.addPairedAsyncShortcode(name, callback); return; } this.#add(this.universal.pairedShortcodes, name, callback, { description: "Universal Paired Shortcode", functionName: "addPairedShortcode", }); this.addPairedNunjucksShortcode(name, callback); this.addPairedLiquidShortcode(name, callback); this.addPairedJavaScriptShortcode(name, callback); } // Related: #498 addPairedAsyncShortcode(name, callback) { this.#add(this.universal.pairedShortcodes, name, callback, { description: "Universal Paired Async Shortcode", functionName: "addPairedAsyncShortcode", }); this.addPairedNunjucksAsyncShortcode(name, callback); this.addPairedLiquidShortcode(name, callback); this.addPairedJavaScriptShortcode(name, callback); } addPairedNunjucksAsyncShortcode(name, callback) { this.#add(this.nunjucks.asyncPairedShortcodes, name, callback, { description: "Nunjucks Async Paired Shortcode", functionName: "addPairedNunjucksAsyncShortcode", }); } addPairedNunjucksShortcode(name, callback, isAsync = false) { if (isAsync) { this.addPairedNunjucksAsyncShortcode(name, callback); } else { this.#add(this.nunjucks.pairedShortcodes, name, callback, { description: "Nunjucks Paired Shortcode", functionName: "addPairedNunjucksShortcode", }); } } addPairedLiquidShortcode(name, callback) { this.#add(this.liquid.pairedShortcodes, name, callback, { description: "Liquid Paired Shortcode", functionName: "addPairedLiquidShortcode", }); } addJavaScriptShortcode(name, callback) { this.#add(this.javascript.shortcodes, name, callback, { description: "JavaScript Shortcode", functionName: "addJavaScriptShortcode", }); // Backwards compat for a time before `addJavaScriptShortcode` existed. this.addJavaScriptFunction(name, callback); } addPairedJavaScriptShortcode(name, callback) { this.#add(this.javascript.pairedShortcodes, name, callback, { description: "JavaScript Paired Shortcode", functionName: "addPairedJavaScriptShortcode", }); // Backwards compat for a time before `addJavaScriptShortcode` existed. this.addJavaScriptFunction(name, callback); } // Both Filters and shortcodes feed into this addJavaScriptFunction(name, callback) { this.#add(this.javascript.functions, name, callback, { description: "JavaScript Function", functionName: "addJavaScriptFunction", }); } /* * Custom Tags */ // tagCallback: function(liquidEngine) { return { parse: …, render: … }} }; addLiquidTag(name, tagFn) { if (typeof tagFn !== "function") { throw new UserConfigError( `EleventyConfig.addLiquidTag expects a callback function to be passed in for ${name}: addLiquidTag(name, function(liquidEngine) { return { parse: …, render: … } })`, ); } this.#add(this.liquid.tags, name, tagFn, { description: "Liquid Custom Tag", functionName: "addLiquidTag", }); } addNunjucksTag(name, tagFn) { if (typeof tagFn !== "function") { throw new UserConfigError( `EleventyConfig.addNunjucksTag expects a callback function to be passed in for ${name}: addNunjucksTag(name, function(nunjucksEngine) {})`, ); } this.#add(this.nunjucks.tags, name, tagFn, { description: "Nunjucks Custom Tag", functionName: "addNunjucksTag", }); } /* * Plugins */ // Internal method _enablePluginExecution() { this.#pluginExecution = true; } // Internal method _disablePluginExecution() { this.#pluginExecution = false; } /* Config is executed in two stages and plugins are the second stage—are we in the plugins stage? */ isPluginExecution() { return this.#pluginExecution; } /** * @typedef {function|Promise|object} PluginDefinition * @property {Function} [configFunction] * @property {string} [eleventyPackage] * @property {object} [eleventyPluginOptions={}] * @property {boolean} [eleventyPluginOptions.unique] */ /** * addPlugin: async friendly in 3.0 * * @param {PluginDefinition} plugin */ addPlugin(plugin, options = {}) { // First addPlugin of a unique plugin wins if (plugin?.eleventyPluginOptions?.unique && this.hasPlugin(plugin)) { debug("Skipping duplicate unique addPlugin for %o", this._getPluginName(plugin)); return; } if (this.isPluginExecution() || options?.immediate) { // this might return a promise return this._executePlugin(plugin, options); } else { this.plugins.push({ plugin, options, pluginNamespace: this.activeNamespace, }); } } /** @param {string} name */ resolvePlugin(name) { return resolvePlugin(name); } /** @param {string|PluginDefinition} plugin */ hasPlugin(plugin) { let pluginName; if (typeof plugin === "string") { pluginName = plugin; } else { pluginName = this._getPluginName(plugin); } return this.plugins.some((entry) => this._getPluginName(entry.plugin) === pluginName); } addNunjucksLoader(options) { if (!isPlainObject(options)) { throw new Error("addNunjucksLoader expects an object literal argument."); } this.nunjucks.loaders.push(options); } // Using Function.name https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#examples /** @param {PluginDefinition} plugin */ _getPluginName(plugin) { if (plugin?.eleventyPackage) { return plugin.eleventyPackage; } if (typeof plugin === "function") { return plugin.name; } if (plugin?.configFunction && typeof plugin.configFunction === "function") { return plugin.configFunction.name; } } // Starting in 3.0 the plugin callback might be asynchronous! _executePlugin(plugin, options) { let name = this._getPluginName(plugin); let ret; debug(`Adding %o plugin`, name || "anonymous"); let pluginBenchmark = this.benchmarks.aggregate.get("Configuration addPlugin"); if (typeof plugin === "function") { pluginBenchmark.before(); this.benchmarks.config; let configFunction = plugin; ret = configFunction(this, options); pluginBenchmark.after(); } else if (plugin?.configFunction) { pluginBenchmark.before(); if (options && typeof options.init === "function") { // init is not yet async-friendly but it’s also barely used options.init.call(this, plugin.initArguments || {}); } ret = plugin.configFunction(this, options); pluginBenchmark.after(); } else { throw new UserConfigError( "Invalid eleventyConfig.addPlugin() signature. Should be a function or a valid Eleventy plugin object.", ); } return ret; } /** @param {string} name */ getNamespacedName(name) { return this.activeNamespace + name; } async namespace(pluginNamespace, callback) { let validNamespace = pluginNamespace && typeof pluginNamespace === "string"; if (validNamespace) { this.activeNamespace = pluginNamespace || ""; } await callback(this); if (validNamespace) { this.activeNamespace = ""; } } /** * Adds a path to a file or directory to the list of pass-through copies * which are copied as-is to the output. * * @param {string|object} fileOrDir The path to the file or directory that should * be copied. OR an object where the key is the input glob and the property is the output directory * @param {object} copyOptions options for recursive-copy. * see https://www.npmjs.com/package/recursive-copy#arguments * default options are defined in TemplatePassthrough copyOptionsDefault * @returns {any} a reference to the `EleventyConfig` object. */ addPassthroughCopy(fileOrDir, copyOptions = {}) { if (copyOptions.mode) { if (copyOptions.mode !== "html-relative") { throw new Error( "Invalid `mode` option for `addPassthroughCopy`. Received: '" + copyOptions.mode + "'", ); } if (isPlainObject(fileOrDir)) { throw new Error( "mode: 'html-relative' does not yet support passthrough copy objects (input -> output mapping). Use a string glob or an Array of string globs.", ); } this.passthroughCopiesHtmlRelative?.add({ match: fileOrDir, ...copyOptions, }); } else if (typeof fileOrDir === "string") { this.passthroughCopies[fileOrDir] = { outputPath: true, copyOptions }; } else { for (let [inputPath, outputPath] of Object.entries(fileOrDir)) { this.passthroughCopies[inputPath] = { outputPath, copyOptions }; } } return this; } /* * Template Formats */ setTemplateFormats(templateFormats) { this.templateFormats = templateFormats; } // additive, usually for plugins addTemplateFormats(templateFormats) { this.templateFormatsAdded.push(templateFormats); } /* * Library Overrides and Options */ setLibrary(engineName, libraryInstance) { if (engineName === "liquid" && Object.keys(this.liquid.options).length) { debug( "WARNING: using `eleventyConfig.setLibrary` will override any configuration set using `.setLiquidOptions` via the config API. You’ll need to pass these options to the library yourself.", ); } else if (engineName === "njk" && Object.keys(this.nunjucks.environmentOptions).length) { debug( "WARNING: using `eleventyConfig.setLibrary` will override any configuration set using `.setNunjucksEnvironmentOptions` via the config API. You’ll need to pass these options to the library yourself.", ); } this.libraryOverrides[engineName.toLowerCase()] = libraryInstance; } /* These callbacks run on both libraryOverrides and default library instances */ amendLibrary(engineName, callback) { let name = engineName.toLowerCase(); if (!this.libraryAmendments[name]) { this.libraryAmendments[name] = []; } this.libraryAmendments[name].push(callback); } setLiquidOptions(options) { this.liquid.options = options; } setLiquidParameterParsing(behavior) { if (behavior !== "legacy" && behavior !== "builtin") { throw new Error( `Invalid argument passed to \`setLiquidParameterParsing\`. Expected one of "legacy" or "builtin".`, ); } this.liquid.parameterParsing = behavior; } setNunjucksEnvironmentOptions(options) { this.nunjucks.environmentOptions = options; } setNunjucksPrecompiledTemplates(templates) { this.nunjucks.precompiledTemplates = templates; } setDynamicPermalinks(enabled) { this.dynamicPermalinks = !!enabled; } setUseGitIgnore(enabled) { this.useGitIgnore = !!enabled; } addWatchTarget(additionalWatchTargets, options = {}) { // Reset the config when the target path changes if (options.resetConfig) { this.watchTargetsConfigReset.add(additionalWatchTargets); } this.additionalWatchTargets.push(additionalWatchTargets); } setWatchJavaScriptDependencies(watchEnabled) { this.watchJavaScriptDependencies = !!watchEnabled; } setServerOptions(options = {}, override = false) { if (override) { this.serverOptions = options; } else { this.serverOptions = DeepCopy(this.serverOptions, options); } } setChokidarConfig(options = {}) { this.chokidarConfig = options; } setWatchThrottleWaitTime(time = 0) { this.watchThrottleWaitTime = time; } // 3.0 change: this does a top level merge instead of reset. setFrontMatterParsingOptions(options = {}) { DeepCopy(this.frontMatterParsingOptions, options); } /* Internal method for CLI --quiet */ _setQuietModeOverride(quietMode) { this.setQuietMode(quietMode); this.#quietModeLocked = true; } setQuietMode(quietMode) { if (this.#quietModeLocked) { debug( "Attempt to `setQuietMode(%o)` ignored, --quiet command line argument override in place.", !!quietMode, ); // override via CLI takes precedence return; } this.quietMode = !!quietMode; } addEngine(fileExtension, classInstance) { if (Object.getPrototypeOf(classInstance).name !== "TemplateEngine") { throw new Error( `Instance of TemplateEngine expected. Received: ${Object.getPrototypeOf(classInstance).name} If you’re trying to create a custom template engine, please use the eleventyConfig.addExtension API.`, ); } // TODO check for conflicts this.extensionMapClasses[fileExtension] = classInstance; } addExtension(fileExtension, options = {}) { let extensions; // Array support 2.0.0-canary.19+ if (Array.isArray(fileExtension)) { extensions = fileExtension; } else { // Comma separated support 4.0.0-alpha.7+ extensions = (fileExtension || "").split(","); } for (let extension of extensions) { if (this.extensionConflictMap[extension]) { throw new Error( `An attempt was made to override the "${extension}" template syntax twice (via the \`addExtension\` configuration API). A maximum of one override is currently supported.`, ); } this.extensionConflictMap[extension] = true; /** @type {object} */ let extensionOptions = Object.assign( { // Might be overridden for aliasing in options.key key: extension, extension: extension, }, options, ); if (extensionOptions.key !== extensionOptions.extension) { extensionOptions.aliasKey = extensionOptions.extension; } this.extensionMap.add(extensionOptions); } } addDataExtension(extensionList, parser) { let options = {}; // second argument is an object with a `parser` callback if (typeof parser !== "function") { if (!("parser" in parser)) { throw new Error( "Expected `parser` property in second argument object to `eleventyConfig.addDataExtension`", ); } options = parser; parser = options.parser; } let extensions = extensionList.split(",").map((s) => s.trim()); for (let extension of extensions) { this.dataExtensions.set(extension, { extension, parser, options, }); } } setUseTemplateCache(bypass) { this.useTemplateCache = !!bypass; } setPrecompiledCollections(collections) { this.precompiledCollections = collections; } // "passthrough" is the default, no other value is explicitly required in code // but opt-out via "copy" is suggested setServerPassthroughCopyBehavior(behavior) { this.serverPassthroughCopyBehavior = behavior; } // Url transforms change page.url and work good with server side content-negotiation (e.g. i18n plugin) addUrlTransform(callback) { this.urlTransforms.push(callback); } setDataFileSuffixes(suffixArray) { this.dataFileSuffixesOverride = suffixArray; } setDataFileBaseName(baseName) { this.dataFileDirBaseNameOverride = baseName; } addTemplate(virtualInputPath, content, data) { // Lookups keys must be normalized virtualInputPath = TemplatePath.stripLeadingDotSlash( TemplatePath.standardizeFilePath(virtualInputPath), ); if (this.virtualTemplates[virtualInputPath]) { throw new Error( "Virtual template conflict: you can’t add multiple virtual templates that have the same inputPath: " + virtualInputPath, ); } this.virtualTemplates[virtualInputPath] = { inputPath: virtualInputPath, data, content, }; } isVirtualTemplate(virtualInputPath) { return Boolean(this.virtualTemplates[virtualInputPath]); } #setDirectory(key, dir) { if (this.isPluginExecution()) { throw new Error( "The `set*Directory` configuration API methods are not yet allowed in plugins.", ); } this.directoryAssignments[key] = dir; } setInputDirectory(dir) { this.#setDirectory("input", dir); } setOutputDirectory(dir) { this.#setDirectory("output", dir); } setDataDirectory(dir) { this.#setDirectory("data", dir); } setIncludesDirectory(dir) { this.#setDirectory("includes", dir); } setLayoutsDirectory(dir) { this.#setDirectory("layouts", dir); } // Some data keywords in Eleventy are reserved, throw an error if an application tries to set these. setFreezeReservedData(bool) { this.freezeReservedData = !!bool; } addDateParsing(callback) { if (typeof callback === "function") { this.customDateParsingCallbacks.add(callback); } else { throw new Error("addDateParsing expects a function argument."); } } // 3.0.0-alpha.18 started merging conflicts here (when possible), issue #3389 addGlobalData(name, data) { name = this.getNamespacedName(name); if (this.globalData[name]) { if (isPlainObject(this.globalData[name]) && isPlainObject(data)) { DeepCopy(this.globalData[name], data); } else { debug("Warning: overwriting a previous value set with addGlobalData(%o)", name); this.globalData[name] = data; } } else { this.globalData[name] = data; } return this; } addNunjucksGlobal(name, globalType) { name = this.getNamespacedName(name); if (this.nunjucks.globals[name]) { debug( chalk.yellow("Warning, overwriting a Nunjucks global with `addNunjucksGlobal(%o)`"), name, ); } if (typeof globalType === "function") { this.nunjucks.globals[name] = this.#decorateCallback(`"${name}" Nunjucks Global`, globalType); } else { this.nunjucks.globals[name] = globalType; } } addTransform(name, callback) { name = this.getNamespacedName(name); this.transforms[name] = this.#decorateCallback(`"${name}" Transform`, callback); } addPreprocessor(name, fileExtensions, callback) { name = this.getNamespacedName(name); this.preprocessors[name] = { filter: fileExtensions, callback: this.#decorateCallback(`"${name}" Preprocessor`, callback), }; } addLinter(name, callback) { name = this.getNamespacedName(name); this.linters[name] = this.#decorateCallback(`"${name}" Linter`, callback); } addLayoutAlias(from, to) { this.layoutAliases[from] = to; } setLayoutResolution(resolution) { this.layoutResolution = !!resolution; } // compat enableLayoutResolution() { this.layoutResolution = true; } configureErrorReporting(options = {}) { // allowMissingExtensions: true Object.assign(this.errorReporting, options); } /* * Collections */ // get config defined collections getCollections() { return this.collections; } addCollection(name, callback) { name = this.getNamespacedName(name); if (this.collections[name]) { throw new UserConfigError( `config.addCollection(${name}) already exists. Try a different name for your collection.`, ); } this.collections[name] = callback; } augmentFunctionContext(fn, options) { let t = typeof fn; if (t !== "function") { throw new UserConfigError( "Invalid type passed to `augmentFunctionContext`—function was expected and received: " + t, ); } return augmentFunction(fn, options); } setConcurrency(number) { if (typeof number !== "number") { throw new UserConfigError("Argument passed to `setConcurrency` must be a number."); } this.#concurrency = number; } getConcurrency() { return this.#concurrency; } getMergingConfigObject() { let obj = { // filters removed in 1.0 (use addTransform instead) transforms: this.transforms, linters: this.linters, preprocessors: this.preprocessors, globalData: this.globalData, layoutAliases: this.layoutAliases, layoutResolution: this.layoutResolution, passthroughCopiesHtmlRelative: this.passthroughCopiesHtmlRelative, passthroughCopies: this.passthroughCopies, // Liquid liquidOptions: this.liquid.options, liquidTags: this.liquid.tags, liquidFilters: this.liquid.filters, liquidShortcodes: this.liquid.shortcodes, liquidPairedShortcodes: this.liquid.pairedShortcodes, liquidParameterParsing: this.liquid.parameterParsing, // Nunjucks nunjucksEnvironmentOptions: this.nunjucks.environmentOptions, nunjucksLoaders: this.nunjucks.loaders, nunjucksPrecompiledTemplates: this.nunjucks.precompiledTemplates, nunjucksFilters: this.nunjucks.filters, nunjucksAsyncFilters: this.nunjucks.asyncFilters, nunjucksTags: this.nunjucks.tags, nunjucksGlobals: this.nunjucks.globals, nunjucksAsyncShortcodes: this.nunjucks.asyncShortcodes, nunjucksShortcodes: this.nunjucks.shortcodes, nunjucksAsyncPairedShortcodes: this.nunjucks.asyncPairedShortcodes, nunjucksPairedShortcodes: this.nunjucks.pairedShortcodes, // 11ty.js javascriptFunctions: this.javascript.functions, // filters and shortcodes, combined javascriptShortcodes: this.javascript.shortcodes, javascriptPairedShortcodes: this.javascript.pairedShortcodes, javascriptFilters: this.javascript.filters, // Markdown markdownHighlighter: this.markdownHighlighter, libraryOverrides: this.libraryOverrides, dynamicPermalinks: this.dynamicPermalinks, useGitIgnore: this.useGitIgnore, ignores: this.ignores, watchIgnores: this.watchIgnores, watchJavaScriptDependencies: this.watchJavaScriptDependencies, additionalWatchTargets: this.additionalWatchTargets, watchTargetsConfigReset: this.watchTargetsConfigReset, serverOptions: this.serverOptions, chokidarConfig: this.chokidarConfig, watchThrottleWaitTime: this.watchThrottleWaitTime, frontMatterParsingOptions: this.frontMatterParsingOptions, dataExtensions: this.dataExtensions, extensionMap: this.extensionMap, extensionMapClasses: this.extensionMapClasses, quietMode: this.quietMode, events: this.events, benchmarkManager: this.benchmarkManager, plugins: this.plugins, useTemplateCache: this.useTemplateCache, precompiledCollections: this.precompiledCollections, dataFilterSelectors: this.dataFilterSelectors, libraryAmendments: this.libraryAmendments, serverPassthroughCopyBehavior: this.serverPassthroughCopyBehavior, urlTransforms: this.urlTransforms, virtualTemplates: this.virtualTemplates, // `directories` and `directoryAssignments` are merged manually prior to plugin processing freezeReservedData: this.freezeReservedData, customDateParsing: this.customDateParsingCallbacks, errorReporting: this.errorReporting, }; if (Array.isArray(this.dataFileSuffixesOverride)) { // no upstream merging of this array, so we add the override: prefix obj["override:dataFileSuffixes"] = this.dataFileSuffixesOverride; } if (this.dataFileDirBaseNameOverride) { obj.dataFileDirBaseNameOverride = this.dataFileDirBaseNameOverride; } // htmlTemplateEngine and markdownTemplateEngine are merged manually in TemplateConfig for config() ordering return obj; } // Removed features get DateTime() { throw new Error( 'Luxon’s DateTime property in configuration was removed in Eleventy v4. Please `import { DateTime } from "luxon"` directly.', ); } _normalizeTemplateFormats() { throw new Error("The internal _normalizeTemplateFormats() method was removed in Eleventy v3"); } setBrowserSyncConfig() { this._attemptedBrowserSyncUse = true; debug( "The `setBrowserSyncConfig` method was removed in Eleventy v2. Use `setServerOptions` with the new Eleventy development server or the `@11ty/eleventy-browser-sync` plugin moving forward.", ); } configureTemplateHandling(options = {}) { // Was used for sync/async swapping on file write operations throw new Error("Internal configuration API method `configureTemplateHandling` was removed."); } setDataDeepMerge(deepMerge) { if (Boolean(deepMerge) === false) { throw new Error( "The `setDataDeepMerge(false)` Configuration API feature was removed in Eleventy v4. Read more at https://github.com/11ty/eleventy/issues/3937", ); } } // No-op functions for backwards compat addHandlebarsHelper() {} setPugOptions() {} setEjsOptions() {} addHandlebarsShortcode() {} addPairedHandlebarsShortcode() {} // Used by the Upgrade Helper Plugin v1 (no longer relevant) // https://github.com/11ty/eleventy-upgrade-help/blob/v1.x/src/data-deep-merge.js#L5-L9 isDataDeepMergeModified() {} } export default UserConfig; ================================================ FILE: src/Util/ArrayUtil.js ================================================ export function arrayDelete(arr, match) { if (!Array.isArray(arr)) { return []; } if (!match) { return arr; } // only mutates if found if (typeof match === "function") { if (arr.find(match)) { return arr.filter((entry) => { return !match(entry); }); } } else if (arr.includes(match)) { return arr.filter((entry) => { return entry !== match; }); } return arr; } ================================================ FILE: src/Util/AsyncEventEmitter.js ================================================ import { EventEmitter } from "node:events"; /** * This class emits events asynchronously. * * Note that Eleventy has two separate event emitter instances it uses: * 1. a userland one (UserConfig.js) * 2. a global one for internals (EventBus.js) */ class AsyncEventEmitter extends EventEmitter { #handlerMode = "parallel"; // TypeScript slop constructor(...args) { super(...args); } reset() { // `eleventy#` event type listeners are removed at the start of each build (singletons) for (let type of this.eventNames()) { if (typeof type === "string" && type.startsWith("eleventy#")) { this.removeAllListeners(type); } } } /** * @param {string} type - The event name to emit. * @param {...*} args - Additional arguments that get passed to listeners. * @returns {Promise} - Promise resolves once all listeners were invoked */ /** @ts-expect-error */ async emit(type, ...args) { let listeners = this.listeners(type); if (listeners.length === 0) { return []; } if (this.#handlerMode == "sequential") { const result = []; for (const listener of listeners) { const returnValue = await listener.apply(this, args); result.push(returnValue); } return result; } else { return Promise.all( listeners.map((listener) => { return listener.apply(this, args); }), ); } } /** * @param {string} type - The event name to emit. * @param {...*} args - Additional lazy-executed function arguments that get passed to listeners. * @returns {Promise} - Promise resolves once all listeners were invoked */ async emitLazy(type, ...args) { let listeners = this.listeners(type); if (listeners.length === 0) { return []; } let argsMap = []; for (let arg of args) { if (typeof arg === "function") { let r = arg(); if (r instanceof Promise) { r = await r; } argsMap.push(r); } else { argsMap.push(arg); } } return this.emit.call(this, type, ...argsMap); } setHandlerMode(mode) { this.#handlerMode = mode; } } export default AsyncEventEmitter; ================================================ FILE: src/Util/Compatibility.js ================================================ import { satisfies } from "../Adapters/Packages/semver.js"; import { getEleventyPackageJson, getWorkingProjectPackageJson } from "./ImportJsonSync.js"; const pkg = getEleventyPackageJson(); // Used in user config versionCheck method. class Compatibility { static NORMALIZE_PRERELEASE_REGEX = /-canary\b/g; static #projectPackageJson; constructor(compatibleRange) { this.compatibleRange = Compatibility.getCompatibilityValue(compatibleRange); } static get projectPackageJson() { if (!this.#projectPackageJson) { this.#projectPackageJson = getWorkingProjectPackageJson(); } return this.#projectPackageJson; } static normalizeIdentifier(identifier) { return identifier.replace(Compatibility.NORMALIZE_PRERELEASE_REGEX, "-alpha"); } static getCompatibilityValue(compatibleRange) { if (compatibleRange) { return compatibleRange; } // fetch from project’s package.json if (this.projectPackageJson?.["11ty"]?.compatibility) { return this.projectPackageJson["11ty"].compatibility; } } isCompatible() { return Compatibility.satisfies(pkg.version, this.compatibleRange); } static satisfies(version, compatibleRange) { return satisfies( Compatibility.normalizeIdentifier(version), Compatibility.normalizeIdentifier(compatibleRange), { includePrerelease: true, }, ); } getErrorMessage() { return `We found Eleventy version '${pkg.version}' which does not meet the required version range: '${this.compatibleRange}'. Use \`npm install @11ty/eleventy\` to upgrade your local project to the latest Eleventy version (or \`npm install @11ty/eleventy -g\` to upgrade the globally installed version).`; } } export default Compatibility; ================================================ FILE: src/Util/ConsoleLogger.js ================================================ import debugUtil from "debug"; import chalk from "../Adapters/Packages/chalk.js"; const debug = debugUtil("Eleventy:Logger"); /** * Logger implementation that logs to STDOUT. * @typedef {'error'|'log'|'warn'|'info'} LogType */ class ConsoleLogger { /** @type {boolean} */ #isVerbose = true; /** @type {boolean} */ #isChalkEnabled = true; /** @type {object|boolean|undefined} */ #logger; constructor() {} isLoggingEnabled() { if (!this.isVerbose || process.env.DEBUG) { return true; } return this.#logger !== false; } get isVerbose() { return this.#isVerbose; } set isVerbose(verbose) { this.#isVerbose = !!verbose; } get isChalkEnabled() { return this.#isChalkEnabled; } set isChalkEnabled(enabled) { this.#isChalkEnabled = !!enabled; } overrideLogger(logger) { this.#logger = logger; } get logger() { return this.#logger || console; } /** @param {string} msg */ log(msg) { this.message(msg); } /** * @typedef LogOptions * @property {string} message * @property {string=} prefix * @property {LogType=} type * @property {string=} color * @property {boolean=} force * @param {LogOptions} options */ logWithOptions({ message, type, prefix, color, force }) { this.message(message, type, color, force, prefix); } /** @param {string} msg */ forceLog(msg) { this.message(msg, undefined, undefined, true); } /** @param {string} msg */ info(msg) { this.message(msg, "log", "blue"); } /** @param {string} msg */ warn(msg) { this.message(msg, "warn", "yellow"); } /** @param {string} msg */ error(msg) { this.message(msg, "error", "red"); } /** * Formats the message to log. * * @param {string} message - The raw message to log. * @param {LogType} [type='log'] - The error level to log. * @param {string|undefined} [chalkColor=undefined] - Color name or falsy to disable * @param {boolean} [forceToConsole=false] - Enforce a log on console instead of specified target. */ message( message, type = "log", chalkColor = undefined, forceToConsole = false, prefix = "[11ty]", ) { if (!forceToConsole && (!this.isVerbose || process.env.DEBUG)) { debug(message); } else if (this.#logger !== false) { message = `${chalk.gray(prefix)} ${message.split("\n").join(`\n${chalk.gray(prefix)} `)}`; if (chalkColor && this.isChalkEnabled) { this.logger[type](chalk[chalkColor](message)); } else { this.logger[type](message); } } } } export default ConsoleLogger; ================================================ FILE: src/Util/DateParse.js ================================================ import debugUtil from "debug"; import { IsoDate } from "@11ty/parse-date-strings"; const debug = debugUtil("Eleventy:DateTime"); export function fromISOtoDateUTC(dateValue, inputPath) { // This has had a UTC default since the beginnning: // https://github.com/11ty/eleventy/commit/4272311dab203d2b217ebd4f6b597eb0e816006b try { let date = IsoDate.parse(dateValue); debug("@11ty/parse-date-strings parsed %o as %o", dateValue, date); return date; } catch (e) { throw new Error( `Data cascade value for \`date\` (${dateValue}) is invalid${inputPath ? ` for ${inputPath}` : ""}`, { cause: e }, ); } } ================================================ FILE: src/Util/DirContains.js ================================================ import path from "node:path"; // Returns true if subfolder is in parent (accepts absolute or relative paths for both) export default function (parentFolder, subFolder) { // path.resolve returns an absolute path if (path.resolve(subFolder).startsWith(path.resolve(parentFolder))) { return true; } return false; } ================================================ FILE: src/Util/EsmResolver.js ================================================ import debugUtil from "debug"; import { fileURLToPath } from "../Adapters/Packages/url.js"; import PathNormalizer from "./PathNormalizer.js"; const debug = debugUtil("Eleventy:EsmResolver"); let lastModifiedPaths = new Map(); export async function initialize({ port }) { // From `eleventy.importCacheReset` event in Require.js port.on("message", ({ path, newDate }) => { lastModifiedPaths.set(path, newDate); }); } // Fixes issue https://github.com/11ty/eleventy/issues/3270 // Docs: https://nodejs.org/docs/latest/api/module.html#resolvespecifier-context-nextresolve export async function resolve(specifier, context, nextResolve) { try { // Not a relative import and not a file import // Or from node_modules (perhaps better to check if the specifier is in the project directory instead) if ( (!specifier.startsWith("../") && !specifier.startsWith("./") && !specifier.startsWith("file:")) || context.parentURL.includes("/node_modules/") ) { return nextResolve(specifier); } let fileUrl = new URL(specifier, context.parentURL); if (fileUrl.searchParams.has("_cache_bust")) { // already is cache busted outside resolver (wider compat, url was changed prior to import, probably in Require.js) return nextResolve(specifier); } let absolutePath = PathNormalizer.normalizeSeperator(fileURLToPath(fileUrl)); // Bust the import cache if this is a recently modified file if (lastModifiedPaths.has(absolutePath)) { fileUrl.search = ""; // delete existing searchparams fileUrl.searchParams.set("_cache_bust", lastModifiedPaths.get(absolutePath)); debug("Cache busting %o to %o", specifier, fileUrl.toString()); return nextResolve(fileUrl.toString()); } } catch (e) { debug("EsmResolver Error parsing specifier (%o): %o", specifier, e); } return nextResolve(specifier); } // export async function load(url, context, nextLoad) { // } ================================================ FILE: src/Util/EsmResolverPortAdapter.core.js ================================================ // This is optional (and featured tested upstream) export const port1 = undefined; ================================================ FILE: src/Util/EsmResolverPortAdapter.js ================================================ import module from "node:module"; import { MessageChannel } from "node:worker_threads"; const { port1, port2 } = new MessageChannel(); // ESM Cache Buster is an enhancement that works in Node 18.19+ // https://nodejs.org/docs/latest/api/module.html#moduleregisterspecifier-parenturl-options // Fixes https://github.com/11ty/eleventy/issues/3270 // ENV variable for https://github.com/11ty/eleventy/issues/3371 if ("register" in module && !process?.env?.ELEVENTY_SKIP_ESM_RESOLVER) { module.register("./EsmResolver.js", import.meta.url, { parentURL: import.meta.url, data: { port: port2, }, transferList: [port2], }); } export { port1 }; ================================================ FILE: src/Util/EventBusUtil.js ================================================ import eventBus from "../EventBus.js"; import debugUtil from "debug"; const debug = debugUtil("Eleventy:EventBus"); class EventBusUtil { static debugCurrentListenerCounts() { for (let name of eventBus.eventNames()) { debug("Listeners for %o: %o", name, eventBus.listenerCount(name)); } } } export default EventBusUtil; ================================================ FILE: src/Util/ExistsCache.js ================================================ import { existsSync, statSync } from "node:fs"; import { TemplatePath } from "@11ty/eleventy-utils"; // Checks both files and directories class ExistsCache { #exists = new Map(); #dirs = new Map(); constructor() { this.lookupCount = 0; } reset() { this.#exists = new Map(); this.#dirs = new Map(); } get size() { return this.#exists.size; } has(path) { return this.#exists.has(path); } set(path, isExist) { this.#exists.set(TemplatePath.addLeadingDotSlash(path), Boolean(isExist)); } // Not yet needed // setDirectory(path, isExist) {} // Relative paths (to root directory) expected (but not enforced due to perf costs) exists(path) { if (!this.#exists.has(path)) { let exists = existsSync(path); this.lookupCount++; // mark for next time this.#exists.set(path, Boolean(exists)); return exists; } return this.#exists.get(path); } isDirectory(path) { if (!this.exists(path)) { return false; } if (!this.#dirs.has(path)) { let isDir = statSync(path).isDirectory(); this.lookupCount++; // mark for next time this.#dirs.set(path, isDir); return isDir; } return this.#dirs.get(path); } } export default ExistsCache; ================================================ FILE: src/Util/FeatureTests.cjs ================================================ function canRequireTypeScript() { try { let res = require("./TypeScript/TypeScriptSample.cts"); return typeof res === "function"; } catch(e) { // Not supported in node_modules, but we know it is supported! if(e.code === "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING") { return true; } return false; } } const TYPESCRIPT_ENABLED = canRequireTypeScript(); module.exports.isTypeScriptSupported = function() { return TYPESCRIPT_ENABLED; } ================================================ FILE: src/Util/FeatureTests.core.cjs ================================================ module.exports.isTypeScriptSupported = function() { return false; } ================================================ FILE: src/Util/FilePathUtil.js ================================================ class FilePathUtil { static isMatchingExtension(filepath, fileExtension) { if (!fileExtension) { return false; } if (!(fileExtension || "").startsWith(".")) { fileExtension = "." + fileExtension; } return filepath.endsWith(fileExtension); } static getFileExtension(filepath) { return (filepath || "").split(".").pop(); } } export { FilePathUtil }; ================================================ FILE: src/Util/FileSize.js ================================================ export function readableFileSize(bytes) { // Uses kilobytes and not kibibytes let entries = [ [1e6, "mB"], [1e3, "kB"], ]; for (let [compare, suffix] of entries) { if (Math.abs(bytes) >= compare) { return Math.round(bytes / compare) + suffix; } } return bytes + " bytes"; } ================================================ FILE: src/Util/FileSystemManager.js ================================================ import path from "node:path"; import { mkdirSync, writeFileSync } from "node:fs"; class FileSystemManager { constructor(templateConfig) { if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") { throw new Error( "Internal error: Missing `templateConfig` or was not an instance of `TemplateConfig`.", ); } this.templateConfig = templateConfig; } exists(pathname) { return this.templateConfig.existsCache.exists(pathname); } createDirectoryForFileSync(filePath) { let dir = path.parse(filePath).dir; if (!dir || this.exists(dir)) { return; } mkdirSync(dir, { recursive: true }); } writeFileSync(filePath, content) { // Note: This deliberately uses the synchronous version to avoid // unbounded concurrency: https://github.com/11ty/eleventy/issues/3271 writeFileSync(filePath, content); } } export { FileSystemManager }; ================================================ FILE: src/Util/GetJavaScriptData.js ================================================ import EleventyBaseError from "../Errors/EleventyBaseError.js"; class JavaScriptInvalidDataFormatError extends EleventyBaseError {} export default async function (inst, inputPath, key = "data", options = {}) { let { mixins, isObjectRequired } = Object.assign( { mixins: {}, isObjectRequired: true, }, options, ); if (inst && key in inst) { // get extra data from `data` method, // either as a function or getter or object literal let result = await (typeof inst[key] === "function" ? Object.keys(mixins).length > 0 ? inst[key].call(mixins) : inst[key]() : inst[key]); if (isObjectRequired && typeof result !== "object") { throw new JavaScriptInvalidDataFormatError( `Invalid data format returned from ${inputPath}: typeof ${typeof result}`, ); } return result; } } ================================================ FILE: src/Util/Git.js ================================================ import { spawnAsync } from "./spawn.js"; import memoize from "./MemoizeFunction.js"; const getCreatedTimestamp = memoize(async function (filePath) { try { let timestamp = await spawnAsync( "git", // Formats https://www.git-scm.com/docs/git-log#_pretty_formats // %at author date, UNIX timestamp ["log", "--diff-filter=A", "--follow", "-1", "--format=%at", filePath], ); // parseInt removes trailing \n return parseInt(timestamp, 10) * 1000; } catch (e) { // do nothing } }); const getUpdatedTimestamp = memoize(async function (filePath) { try { let timestamp = await spawnAsync( "git", // Formats https://www.git-scm.com/docs/git-log#_pretty_formats // %at author date, UNIX timestamp ["log", "-1", "--format=%at", filePath], ); return parseInt(timestamp, 10) * 1000; } catch (e) { // do nothing } }); export { getCreatedTimestamp, getUpdatedTimestamp }; ================================================ FILE: src/Util/GlobMatcher.client.js ================================================ export function isGlobMatch() { throw new Error( "Glob matching (e.g. getFilteredByGlob collection API method) is not supported in the `@11ty/client` bundle. Use the `@11ty/client/eleventy` bundle instead.", ); } // When using a glob as an input (see ProjectDirectories) export function isDynamicPattern(pattern) { return false; } ================================================ FILE: src/Util/GlobMatcher.js ================================================ // picomatch costs ~50KB minified import picomatch from "picomatch"; import { TemplatePath } from "@11ty/eleventy-utils"; export function isGlobMatch(filepath, globs = [], options = undefined) { if (!filepath || !Array.isArray(globs) || globs.length === 0) { return false; } let inputPath = TemplatePath.stripLeadingDotSlash(filepath); let opts = Object.assign( { dot: true, nocase: true, // insensitive }, options, ); // globs: string or array of strings return picomatch.isMatch(inputPath, globs, opts); } // via tinyglobby export function isDynamicPattern(pattern) { const s = picomatch.scan(pattern); return s.isGlob || s.negated; } ================================================ FILE: src/Util/GlobRemap.js ================================================ import path from "node:path"; import ProjectDirectories from "./ProjectDirectories.js"; import PathNormalizer from "./PathNormalizer.js"; // even on Windows (in cmd.exe) these paths are normalized to forward slashes // tinyglobby expects forward slashes on Windows const SEP = "/"; class GlobRemap { constructor(paths = []) { this.paths = paths; this.cwd = GlobRemap.getCwd(paths); } getCwd() { return this.cwd; } getRemapped(paths) { return paths.map((entry) => GlobRemap.remapInput(entry, this.cwd)); } getInput() { return this.getRemapped(this.paths); } getOutput(paths = []) { return paths.map((entry) => GlobRemap.remapOutput(entry, this.cwd)); } static getParentDirPrefix(filePath = "") { let count = []; for (let p of filePath.split(SEP)) { if (p === "..") { count.push(".."); } else { break; } } if (count.length > 0) { // trailing slash return count.join(SEP) + SEP; } return ""; } static getLongestParentDirPrefix(filePaths) { let longest = ""; filePaths .map((entry) => { return this.getParentDirPrefix(entry); }) .filter((entry) => Boolean(entry)) .forEach((prefix) => { if (!longest || prefix.length > longest.length) { longest = prefix; } }); return longest; } // alias static getCwd(filePaths) { return this.getLongestParentDirPrefix(filePaths); } static remapInput(entry, cwd) { if (cwd) { if (!entry.startsWith("**" + SEP) && !entry.startsWith(`.git${SEP}**`)) { return PathNormalizer.normalizeSeperator(ProjectDirectories.getRelativeTo(entry, cwd)); } } return entry; } static remapOutput(entry, cwd) { if (cwd) { return PathNormalizer.normalizeSeperator(path.join(cwd, entry)); } return entry; } } export default GlobRemap; ================================================ FILE: src/Util/GlobStripper.js ================================================ // import { TemplatePath } from "@11ty/eleventy-utils"; import { isDynamicPattern } from "./GlobMatcher.js"; export class GlobStripper { static SEP = "/"; static DOUBLE = "**"; static SINGLE = "*"; static parse(pattern = "") { let parts = pattern.split(this.SEP); let c = 0; for (let p of parts) { if (p === "?") { continue; } if ( p === this.DOUBLE || (p.includes(this.SINGLE) && !p.includes(this.DOUBLE)) || isDynamicPattern(p) ) { return { path: parts.slice(0, c).join(this.SEP) || ".", glob: parts.slice(c).join(this.SEP) || undefined, }; } c++; } if (isDynamicPattern(pattern)) { throw new Error( `Could not automatically determine top-most folder from glob pattern: ${pattern}`, ); } return { path: parts.join(this.SEP) || ".", }; } } ================================================ FILE: src/Util/HtmlRelativeCopy.js ================================================ import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import { isValidUrl } from "./UrlUtil.js"; import { isGlobMatch } from "./GlobMatcher.js"; // https://github.com/11ty/eleventy/pull/3573 class HtmlRelativeCopy { #userConfig; #matchingGlobs = new Set(); #matchingGlobsArray; #dirty = false; #paths = new Set(); #failOnError = true; #copyOptions = { dot: false, // differs from standard passthrough copy }; isEnabled() { return this.#matchingGlobs.size > 0; } setFailOnError(failOnError) { this.#failOnError = Boolean(failOnError); } setCopyOptions(opts) { if (opts) { Object.assign(this.#copyOptions, opts); } } setUserConfig(userConfig) { if (!userConfig || userConfig.constructor.name !== "UserConfig") { throw new Error( "Internal error: Missing `userConfig` or was not an instance of `UserConfig`.", ); } this.#userConfig = userConfig; } addPaths(paths = []) { for (let path of paths) { this.#paths.add(TemplatePath.getDir(path)); } } get matchingGlobs() { if (this.#dirty || !this.#matchingGlobsArray) { this.#matchingGlobsArray = Array.from(this.#matchingGlobs); this.#dirty = false; } return this.#matchingGlobsArray; } addMatchingGlob(glob) { if (glob) { if (Array.isArray(glob)) { for (let g of glob) { this.#matchingGlobs.add(g); } } else { this.#matchingGlobs.add(glob); } this.#dirty = true; } } isSkippableHref(rawRef) { if ( this.#matchingGlobs.size === 0 || !rawRef || path.isAbsolute(rawRef) || isValidUrl(rawRef) ) { return true; } return false; } isCopyableTarget(target) { if (!isGlobMatch(target, this.matchingGlobs)) { return false; } return true; } exists(filePath) { return this.#userConfig.exists(filePath); } getAliasedPath(ref) { for (let dir of this.#paths) { let found = TemplatePath.join(dir, ref); if (this.isCopyableTarget(found) && this.exists(found)) { return found; } } } getFilePathRelativeToProjectRoot(ref, contextFilePath) { let dir = TemplatePath.getDirFromFilePath(contextFilePath); return TemplatePath.join(dir, ref); } copy(fileRef, tmplInputPath, tmplOutputPath) { // original ref is a full URL or no globs exist if (this.isSkippableHref(fileRef)) { return; } // Relative to source file’s input path let source = this.getFilePathRelativeToProjectRoot(fileRef, tmplInputPath); if (!this.isCopyableTarget(source)) { return; } if (!this.exists(source)) { // Try to alias using `options.paths` let alias = this.getAliasedPath(fileRef); if (!alias) { if (this.#failOnError) { throw new Error( "Missing input file for `html-relative` Passthrough Copy file: " + TemplatePath.absolutePath(source), ); } // don’t fail on error return; } source = alias; } let target = this.getFilePathRelativeToProjectRoot(fileRef, tmplOutputPath); // We use a Set here to allow passthrough copy manager to properly error on conflicts upstream // Only errors when different inputs write to the same output // Also errors if attempts to write outside the output folder. this.#userConfig.emit("eleventy#copy", { source, target, options: this.#copyOptions, }); } } export { HtmlRelativeCopy }; ================================================ FILE: src/Util/HtmlTransformer.js ================================================ import posthtml from "posthtml"; import urls from "@11ty/posthtml-urls"; import { FilePathUtil } from "./FilePathUtil.js"; import { arrayDelete } from "./ArrayUtil.js"; export class HtmlTransformer { // feature test for Eleventy Bundle Plugin static SUPPORTS_PLUGINS_ENABLED_CALLBACK = true; static TYPES = ["callbacks", "plugins"]; constructor() { // execution order is important (not order of addition/object key order) this.callbacks = {}; this.posthtmlProcessOptions = { recognizeNoValueAttribute: true, closingSingleTag: "closeAs", }; this.plugins = {}; } get aggregateBench() { if (!this.userConfig) { throw new Error("Internal error: Missing `userConfig` in HtmlTransformer."); } return this.userConfig.benchmarkManager.get("Aggregate"); } setUserConfig(config) { this.userConfig = config; } static prioritySort(a, b) { if (b.priority > a.priority) { return 1; } if (a.priority > b.priority) { return -1; } return 0; } // context is important as it is used in html base plugin for page specific URL static _getPosthtmlInstance(callbacks = [], plugins = [], context = {}) { let inst = posthtml(); // already sorted by priority when added for (let { fn: plugin, options } of plugins) { inst.use(plugin(Object.assign({}, context, options))); } // Run the built-ins last if (callbacks.length > 0) { inst.use( urls({ eachURL: (url, attrName, tagName) => { for (let { fn: callback } of callbacks) { // already sorted by priority when added url = callback.call(context, url, { attribute: attrName, tag: tagName }); } return url; }, }), ); } return inst; } _add(extensions, addType, value, options = {}) { options = Object.assign( { priority: 0, }, options, ); let extensionsArray = (extensions || "").split(","); for (let ext of extensionsArray) { let target = this[addType]; if (!target[ext]) { target[ext] = []; } target[ext].push({ // *could* fallback to function name, `value.name` name: options.name, // for `remove` and debugging fn: value, // callback or plugin priority: options.priority, // sorted in descending order enabled: options.enabled || (() => true), options: options.pluginOptions, }); target[ext].sort(HtmlTransformer.prioritySort); } } addPosthtmlPlugin(extensions, plugin, options = {}) { this._add(extensions, "plugins", plugin, options); } // match can be a plugin function or a filter callback(plugin => true); remove(extensions, match) { for (let removeType of HtmlTransformer.TYPES) { for (let ext of (extensions || "").split(",")) { this[removeType][ext] = arrayDelete(this[removeType][ext], match); } } } addUrlTransform(extensions, callback, options = {}) { this._add(extensions, "callbacks", callback, options); } setPosthtmlProcessOptions(options) { Object.assign(this.posthtmlProcessOptions, options); } isTransformable(extension, context) { return ( this.getCallbacks(extension, context).length > 0 || this.getPlugins(extension).length > 0 ); } getCallbacks(extension, context) { let callbacks = this.callbacks[extension] || []; return callbacks.filter(({ enabled }) => { if (!enabled || typeof enabled !== "function") { return true; } return enabled(context); }); } getPlugins(extension) { let plugins = this.plugins[extension] || []; return plugins.filter(({ enabled }) => { if (!enabled || typeof enabled !== "function") { return true; } return enabled(); }); } static async transformStandalone(content, callback, posthtmlProcessOptions = {}) { let posthtmlInstance = this._getPosthtmlInstance([ { fn: callback, enabled: () => true, }, ]); let result = await posthtmlInstance.process(content, posthtmlProcessOptions); return result.html; } async transformContent(outputPath, content, context) { let extension = FilePathUtil.getFileExtension(outputPath); if (!this.isTransformable(extension, context)) { return content; } let bench = this.aggregateBench.get(`Transforming \`${extension}\` with posthtml`); bench.before(); let callbacks = this.getCallbacks(extension, context); let plugins = this.getPlugins(extension); let posthtmlInstance = HtmlTransformer._getPosthtmlInstance(callbacks, plugins, context); let result = await posthtmlInstance.process(content, this.posthtmlProcessOptions); bench.after(); return result.html; } } ================================================ FILE: src/Util/ImportJsonSync.js ================================================ import { existsSync } from "node:fs"; import debugUtil from "debug"; import { TemplatePath } from "@11ty/eleventy-utils"; import { importJsonSync, eleventyPackageJson } from "./RequireUtils.js"; const debug = debugUtil("Eleventy:ImportJsonSync"); function findFilePathInParentDirs(dir, filename) { // `package.json` searches look in parent dirs: // https://docs.npmjs.com/cli/v7/configuring-npm/folders#more-information // Fixes issue #3178, limited to working dir paths only let workingDir = TemplatePath.getWorkingDir(); // TODO use DirContains let allDirs = TemplatePath.getAllDirs(dir).filter((entry) => entry.startsWith(workingDir)); for (let dir of allDirs) { let newPath = TemplatePath.join(dir, filename); if (existsSync(newPath)) { debug("Found %o searching parent directories at: %o", filename, dir); return newPath; } } } function getEleventyPackageJson() { return eleventyPackageJson; } // Used by EleventyServe.js for custom servers only function getModulePackageJson(dir) { let filePath = findFilePathInParentDirs(TemplatePath.absolutePath(dir), "package.json"); // Fails nicely if (!filePath) { return {}; } return importJsonSync(filePath); } // This will *not* find a package.json in a parent directory above root function getWorkingProjectPackageJsonPath() { let dir = TemplatePath.absolutePath(TemplatePath.getWorkingDir()); return findFilePathInParentDirs(dir, "package.json"); } function getWorkingProjectPackageJson() { let filePath = getWorkingProjectPackageJsonPath(); // Fails nicely if (!filePath) { return {}; } return importJsonSync(filePath); } export { importJsonSync, getEleventyPackageJson, getModulePackageJson, getWorkingProjectPackageJson, findFilePathInParentDirs, getWorkingProjectPackageJsonPath, }; ================================================ FILE: src/Util/IsAsyncFunction.js ================================================ const ComparisonAsyncFunction = (async () => {}).constructor; export default function isAsyncFunction(fn) { return fn instanceof ComparisonAsyncFunction; } ================================================ FILE: src/Util/JavaScriptDependencies.core.js ================================================ class JavaScriptDependencies { static async getDependencies() { throw new Error("This feature is not supported in `@11ty/client` bundles."); } } export default JavaScriptDependencies; ================================================ FILE: src/Util/JavaScriptDependencies.js ================================================ import dependencyTree from "@11ty/dependency-tree"; import { find, findGraph, mergeGraphs } from "@11ty/dependency-tree-esm"; import { find as findTypeScript, findGraph as findTypeScriptGraph, } from "@11ty/dependency-tree-typescript"; import { TemplatePath } from "@11ty/eleventy-utils"; import { DepGraph } from "dependency-graph"; import { union } from "./SetUtil.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; class JavaScriptDependencies { static getErrorMessage(file, type) { return `A problem was encountered looking for JavaScript dependencies in ${type} file: ${file}. This only affects --watch and --serve behavior and does not affect your build.`; } static getFlavor(filePath, isProjectUsingEsm) { if ( (isProjectUsingEsm && (filePath.endsWith(".js") || filePath.endsWith(".ts"))) || filePath.endsWith(".mjs") || filePath.endsWith(".mts") ) { return "esm"; } if ( (!isProjectUsingEsm && (filePath.endsWith(".js") || filePath.endsWith(".ts"))) || filePath.endsWith(".cjs") || filePath.endsWith(".cts") ) { return "cjs"; } } static isTypeScript(filePath) { return filePath.endsWith(".ts") || filePath.endsWith(".cts") || filePath.endsWith(".mts"); } static async getCommonJsDependencies(inputFiles, isProjectUsingEsm) { let depSet = new Set(); // TODO does this need to work with aliasing? what other JS extensions will have deps? let commonJsFiles = inputFiles.filter( (file) => this.getFlavor(file, isProjectUsingEsm) === "cjs", ); // TODO require(esm) means some files *could* be wrong here for (let file of commonJsFiles) { try { let modules = dependencyTree(file, { nodeModuleNames: "exclude", allowNotFound: true, }).map((dependency) => { return TemplatePath.addLeadingDotSlash(TemplatePath.relativePath(dependency)); }); for (let dep of modules) { depSet.add(dep); } } catch (e) { throw new EleventyBaseError(this.getErrorMessage(file, "CommonJS"), e); } } return depSet; } static async getEsmDependencies(inputFiles, isProjectUsingEsm) { let depSet = new Set(); let esmFiles = inputFiles.filter((file) => this.getFlavor(file, isProjectUsingEsm) === "esm"); for (let file of esmFiles) { try { // TODO feature test for node:module->stripTypeScriptTypes and use with find(file, { preprocess }) let modules = await (this.isTypeScript(file) ? findTypeScript : find)(file); for (let dep of modules) { depSet.add(dep); } } catch (e) { throw new EleventyBaseError(this.getErrorMessage(file, "ESM"), e); } } return depSet; } static async getDependencies(inputFiles, isProjectUsingEsm) { let cjs = await this.getCommonJsDependencies(inputFiles, isProjectUsingEsm); let esm = await this.getEsmDependencies(inputFiles, isProjectUsingEsm); return Array.from(union(cjs, esm)); } static async getEsmGraph(inputFiles, isProjectUsingEsm) { let rootGraph = new DepGraph(); let esmFiles = inputFiles.filter((file) => this.getFlavor(file, isProjectUsingEsm) === "esm"); for (let file of esmFiles) { try { // TODO feature test for node:module->stripTypeScriptTypes and use with find(file, { preprocess }) let graph = await (this.isTypeScript(file) ? findTypeScriptGraph : findGraph)(file); mergeGraphs(rootGraph, graph); } catch (e) { throw new EleventyBaseError(this.getErrorMessage(file, "ESM"), e); } } return rootGraph; } } export default JavaScriptDependencies; ================================================ FILE: src/Util/MemoizeFunction.js ================================================ export default function (callback, options = {}) { let { bench, name } = options; let cache = new Map(); return (...args) => { // Only supports single-arg functions for now. if (args.filter(Boolean).length > 1) { bench?.get(`(count) ${name} Not valid for memoize`).incrementCount(); return callback(...args); } let [cacheKey] = args; if (!cache.has(cacheKey)) { cache.set(cacheKey, callback(...args)); bench?.get(`(count) ${name} memoize miss`).incrementCount(); return cache.get(cacheKey); } bench?.get(`(count) ${name} memoize hit`).incrementCount(); return cache.get(cacheKey); }; } ================================================ FILE: src/Util/NewLineAdapter.core.js ================================================ export const EOL = "\n"; ================================================ FILE: src/Util/NewLineAdapter.js ================================================ export { EOL } from "node:os"; ================================================ FILE: src/Util/Objects/DeepFreeze.js ================================================ import { isPlainObject } from "@11ty/eleventy-utils"; // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze function DeepFreeze(obj, topLevelExceptions) { for (let name of Reflect.ownKeys(obj)) { if ((topLevelExceptions || []).find((key) => key === name)) { continue; } const value = obj[name]; if (isPlainObject(value)) { DeepFreeze(value); } } return Object.freeze(obj); } export { DeepFreeze }; ================================================ FILE: src/Util/Objects/ObjectFilter.js ================================================ export default function objectFilter(obj, callback) { let newObject = {}; for (let [key, value] of Object.entries(obj || {})) { if (callback(value, key)) { newObject[key] = value; } } return newObject; } ================================================ FILE: src/Util/Objects/ProxyWrap.js ================================================ import debugUtil from "debug"; import { isPlainObject } from "@11ty/eleventy-utils"; const debug = debugUtil("Dev:Eleventy:Proxy"); const ProxySymbol = Symbol.for("11ty.ProxySymbol"); function wrapObject(target, fallback) { if (Object.isFrozen(target)) { return target; } return new Proxy(target, { getOwnPropertyDescriptor(target, prop) { let ret; if (Reflect.has(target, prop)) { ret = Reflect.getOwnPropertyDescriptor(target, prop); } else if (Reflect.has(fallback, prop)) { ret = Reflect.getOwnPropertyDescriptor(fallback, prop); } return ret; }, has(target, prop) { if (Reflect.has(target, prop)) { return true; } return Reflect.has(fallback, prop); }, ownKeys(target) { let s = new Set(); // The fallback keys need to come first to preserve proper key order // https://github.com/11ty/eleventy/issues/3849 if (isPlainObject(fallback)) { for (let k of Reflect.ownKeys(fallback)) { s.add(k); } } for (let k of Reflect.ownKeys(target)) { if (!s.has(k)) { s.add(k); } } return Array.from(s); }, get(target, prop) { debug("handler:get", prop); if (prop === ProxySymbol) { return true; } let value = Reflect.get(target, prop); if (Reflect.has(target, prop)) { // Careful: swapped from node:util/types->isProxy test here if (Reflect.get(target, ProxySymbol)) { return value; } if (isPlainObject(value) && Reflect.has(fallback, prop)) { if (Object.isFrozen(value)) { return value; } let ret = wrapObject(value, Reflect.get(fallback, prop)); debug("handler:get (primary, object)", prop); return ret; } debug("handler:get (primary)", prop); return value; } // Does not exist in primary if ( (typeof fallback === "object" || typeof fallback === "function") && Reflect.has(fallback, prop) ) { // fallback has prop let fallbackValue = Reflect.get(fallback, prop); if (isPlainObject(fallbackValue)) { if (Object.isFrozen(fallbackValue)) { return fallbackValue; } debug("handler:get (fallback, object)", prop); // set empty object on primary let emptyObject = {}; Reflect.set(target, prop, emptyObject); return wrapObject(emptyObject, fallbackValue); } debug("handler:get (fallback)", prop); return fallbackValue; } // primary *and* fallback do _not_ have prop debug("handler:get (not on primary or fallback)", prop); return value; }, set(target, prop, value) { debug("handler:set", prop); return Reflect.set(target, prop, value); }, }); } function ProxyWrap(target, fallback) { if (!isPlainObject(target) || !isPlainObject(fallback)) { throw new Error("ProxyWrap expects objects for both the target and fallback"); } return wrapObject(target, fallback); } export { ProxyWrap }; ================================================ FILE: src/Util/Objects/SampleModule.mjs ================================================ export default {}; ================================================ FILE: src/Util/Objects/Sortable.js ================================================ class Sortable { constructor() { this.isSortAscending = true; this.isSortNumeric = false; this.items = []; this._dirty = true; this.sortFunctionStringMap = { "A-Z": "sortFunctionAscending", "Z-A": "sortFunctionDescending", "0-9": "sortFunctionNumericAscending", "9-0": "sortFunctionNumericDescending", }; } get length() { return this.items.length; } add(item) { this._dirty = true; this.items.push(item); } sort(sortFunction) { if (!sortFunction) { sortFunction = this.getSortFunction(); } else if (typeof sortFunction === "string") { let key = sortFunction; let name; if (key in this.sortFunctionStringMap) { name = this.sortFunctionStringMap[key]; } if (Sortable[name]) { sortFunction = Sortable[name]; } else { throw new Error( `Invalid String argument for sort(). Received \`${key}\`. Valid values: ${Object.keys( this.sortFunctionStringMap, )}`, ); } } return this.items.slice().sort(sortFunction); } sortAscending() { return this.sort(this.getSortFunctionAscending()); } sortDescending() { return this.sort(this.getSortFunctionDescending()); } setSortDescending(isDescending = true) { this.isSortAscending = !isDescending; } setSortAscending(isAscending = true) { this.isSortAscending = isAscending; } setSortNumeric(isNumeric) { this.isSortNumeric = isNumeric; } /* Sort functions */ static sortFunctionNumericAscending(a, b) { return a - b; } static sortFunctionNumericDescending(a, b) { return b - a; } static sortFunctionAscending(a, b) { if (a > b) { return 1; } else if (a < b) { return -1; } return 0; } static sortFunctionDescending(a, b) { return Sortable.sortFunctionAscending(b, a); } static sortFunctionAlphabeticAscending(a, b) { return Sortable.sortFunctionAscending(a, b); } static sortFunctionAlphabeticDescending(a, b) { return Sortable.sortFunctionAscending(b, a); } static sortFunctionDate(mapA, mapB) { return Sortable.sortFunctionNumericAscending(mapA.date.getTime(), mapB.date.getTime()); } static sortFunctionDateInputPath(mapA, mapB) { let sortDate = Sortable.sortFunctionNumericAscending(mapA.date.getTime(), mapB.date.getTime()); if (sortDate === 0) { return Sortable.sortFunctionAlphabeticAscending(mapA.inputPath, mapB.inputPath); } return sortDate; } /* End sort functions */ getSortFunction() { if (this.isSortAscending) { return this.getSortFunctionAscending(); } else { return this.getSortFunctionDescending(); } } getSortFunctionAscending() { if (this.isSortNumeric) { return Sortable.sortFunctionNumericAscending; } else { return Sortable.sortFunctionAlphabeticAscending; } } getSortFunctionDescending() { if (this.isSortNumeric) { return Sortable.sortFunctionNumericDescending; } else { return Sortable.sortFunctionAlphabeticDescending; } } } export default Sortable; ================================================ FILE: src/Util/Objects/Unique.js ================================================ export default function Unique(arr) { return Array.from(new Set(arr)); } ================================================ FILE: src/Util/PassthroughCopyBehaviorCheck.js ================================================ function isUsingEleventyDevServer(config) { return ( !config.serverOptions.module || config.serverOptions.module === "@11ty/eleventy-dev-server" ); } // Config opt-in via serverPassthroughCopyBehavior // False when other server is used // False when runMode is "build" or "watch" export default function (config, runMode) { return ( config.serverPassthroughCopyBehavior === "passthrough" && isUsingEleventyDevServer(config) && runMode === "serve" ); } ================================================ FILE: src/Util/PathNormalizer.js ================================================ import { parse, sep } from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import { fileURLToPath } from "../Adapters/Packages/url.js"; export default class PathNormalizer { static getParts(inputPath) { if (!inputPath) { return []; } let separator = "/"; if (inputPath.includes(sep)) { separator = sep; } return inputPath.split(separator).filter((entry) => entry !== "."); } // order is important here: the top-most directory returns first // array of file and all parent directories static getAllPaths(inputPath) { let parts = this.getParts(inputPath); let allPaths = []; let fullpath = ""; for (let part of parts) { fullpath += (fullpath.length > 0 ? "/" : "") + part; allPaths.push(fullpath); } return allPaths; } static normalizeSeperator(inputPath) { if (!inputPath) { return inputPath; } return inputPath.split(sep).join("/"); } static addTrailingSlashToDirectory(dir) { if (dir.endsWith("/")) { return dir; } return dir + "/"; } // returns a path static getDirectoryFromFilePath(filePath) { let parsed = parse(filePath); return this.addTrailingSlashToDirectory(parsed.dir); } static fullNormalization(inputPath) { if (typeof inputPath !== "string") { return inputPath; } // Fix file:///Users/ or file:///C:/ paths passed in if (inputPath.startsWith("file://")) { inputPath = fileURLToPath(inputPath); } // Paths should not be absolute (we convert absolute paths to relative) // Paths should not have a leading dot slash // Paths should always be `/` independent of OS path separator return TemplatePath.stripLeadingDotSlash( this.normalizeSeperator(TemplatePath.relativePath(inputPath)), ); } } ================================================ FILE: src/Util/PathPrefixer.js ================================================ import path from "node:path"; import PathNormalizer from "./PathNormalizer.js"; class PathPrefixer { static normalizePathPrefix(pathPrefix) { if (pathPrefix) { // add leading / (for browsersync), see #1454 // path.join uses \\ for Windows so we split and rejoin return PathPrefixer.joinUrlParts("/", pathPrefix); } return "/"; } static joinUrlParts(...parts) { return PathNormalizer.normalizeSeperator(path.join(...parts)); } } export default PathPrefixer; ================================================ FILE: src/Util/Pluralize.js ================================================ export default function (count, singleWord, pluralWord) { return count === 1 ? singleWord : pluralWord; } ================================================ FILE: src/Util/ProjectDirectories.js ================================================ import { existsSync, statSync } from "node:fs"; import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import { isDynamicPattern } from "../Util/GlobMatcher.js"; import DirContains from "./DirContains.js"; /* Directories internally should always use *nix forward slashes */ class ProjectDirectories { static defaults = { input: "./", data: "./_data/", // Relative to input directory includes: "./_includes/", // Relative to input directory layouts: "./_layouts/", // Relative to input directory output: "./_site/", }; // no updates allowed, input/output set via CLI #frozen = false; #raw = {}; #dirs = {}; inputFile = undefined; inputGlob = undefined; // Add leading dot slash // Use forward slashes static normalizePath(fileOrDir) { return TemplatePath.standardizeFilePath(fileOrDir); } // Must be a directory // Always include a trailing slash static normalizeDirectory(dir) { return this.addTrailingSlash(this.normalizePath(dir)); } normalizeDirectoryPathRelativeToInputDirectory(filePath) { return ProjectDirectories.normalizeDirectory(path.join(this.input, filePath)); } static addTrailingSlash(path) { if (path.slice(-1) === "/") { return path; } return path + "/"; } // If input/output are set via CLI, they take precedence over all other configuration values. freeze() { this.#frozen = true; } setViaConfigObject(configDirs = {}) { // input must come last let inputChanged = false; if ( configDirs.input && ProjectDirectories.normalizeDirectory(configDirs.input) !== this.input ) { this.#setInputRaw(configDirs.input); inputChanged = true; } // If falsy or an empty string, the current directory is used. if (configDirs.output !== undefined) { if (ProjectDirectories.normalizeDirectory(configDirs.output) !== this.output) { this.setOutput(configDirs.output); } } // Input relative directory, if falsy or an empty string, inputDir is used! // Always set if input changed, e.g. input is `src` and data is `../_data` (resulting in `./_data`) we still want to set data to this new value if (configDirs.data !== undefined) { if ( inputChanged || this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.data || "") !== this.data ) { this.setData(configDirs.data); } } // Input relative directory, if falsy or an empty string, inputDir is used! if (configDirs.includes !== undefined) { if ( inputChanged || this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.includes || "") !== this.includes ) { this.setIncludes(configDirs.includes); } } // Input relative directory, if falsy or an empty string, inputDir is used! if (configDirs.layouts !== undefined) { if ( inputChanged || this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.layouts || "") !== this.layouts ) { this.setLayouts(configDirs.layouts); } } if (inputChanged) { this.updateInputDependencies(); } } updateInputDependencies() { // raw first, fall back to Eleventy defaults if not yet set this.setData(this.#raw.data ?? ProjectDirectories.defaults.data); this.setIncludes(this.#raw.includes ?? ProjectDirectories.defaults.includes); // Should not include this if not explicitly opted-in if (this.#raw.layouts !== undefined) { this.setLayouts(this.#raw.layouts ?? ProjectDirectories.defaults.layouts); } } /* Relative to project root, must exist */ #setInputRaw(dirOrFile, inputDir = undefined) { // is frozen and was defined previously if (this.#frozen && this.#raw.input !== undefined) { return; } this.#raw.input = dirOrFile; if (!dirOrFile) { // input must exist if inputDir is not set. return; } // Normalize absolute paths to relative, #3805 #3896 if (path.isAbsolute(dirOrFile)) { dirOrFile = path.relative(".", dirOrFile); } // Input has to exist (assumed glob if it does not exist) let inputExists = existsSync(dirOrFile); let inputExistsAndIsDirectory = inputExists && statSync(dirOrFile).isDirectory(); if (inputExistsAndIsDirectory) { // is not a file or glob this.#dirs.input = ProjectDirectories.normalizeDirectory(dirOrFile); } else { if (inputExists) { this.inputFile = ProjectDirectories.normalizePath(dirOrFile); } else { if (!isDynamicPattern(dirOrFile)) { throw new Error( `The "${dirOrFile}" \`input\` parameter (directory or file path) must exist on the file system (unless detected as a glob by the \`tinyglobby\` package)`, ); } this.inputGlob = dirOrFile; } // Explicit Eleventy option for inputDir if (inputDir) { // Changed in 3.0: must exist if (!existsSync(inputDir)) { throw new Error("Directory must exist (via inputDir option to Eleventy constructor)."); } this.#dirs.input = ProjectDirectories.normalizeDirectory(inputDir); } else { // the input directory is implied to be the parent directory of the // file, unless inputDir is explicitly specified (via Eleventy constructor `options`) this.#dirs.input = ProjectDirectories.normalizeDirectory( TemplatePath.getDirFromFilePath(dirOrFile), // works with globs ); } } } setInput(dirOrFile, inputDir = undefined) { this.#setInputRaw(dirOrFile, inputDir); // does not update this.updateInputDependencies(); } /* Relative to input dir */ setIncludes(dir) { if (dir !== undefined) { // falsy or an empty string is valid (falls back to input dir) this.#raw.includes = dir; this.#dirs.includes = ProjectDirectories.normalizeDirectory( TemplatePath.join(this.input, dir || ""), ); } } /* Relative to input dir */ /* Optional */ setLayouts(dir) { if (dir !== undefined) { // falsy or an empty string is valid (falls back to input dir) this.#raw.layouts = dir; this.#dirs.layouts = ProjectDirectories.normalizeDirectory( TemplatePath.join(this.input, dir || ""), ); } } /* Relative to input dir */ setData(dir) { if (dir !== undefined) { // falsy or an empty string is valid (falls back to input dir) // TODO must exist if specified this.#raw.data = dir; this.#dirs.data = ProjectDirectories.normalizeDirectory( TemplatePath.join(this.input, dir || ""), ); } } /* Relative to project root */ setOutput(dir) { // is frozen and was defined previously if (this.#frozen && this.#raw.output !== undefined) { return; } if (dir !== undefined) { this.#raw.output = dir; // Normalize absolute paths to relative, #3805 #3896 if (path.isAbsolute(dir)) { dir = path.relative(".", dir); } this.#dirs.output = ProjectDirectories.normalizeDirectory(dir || ""); } } get input() { return this.#dirs.input || ProjectDirectories.defaults.input; } get data() { return this.#dirs.data || ProjectDirectories.defaults.data; } get includes() { return this.#dirs.includes || ProjectDirectories.defaults.includes; } get layouts() { // explicit opt-in, no fallback. return this.#dirs.layouts; } get output() { return this.#dirs.output || ProjectDirectories.defaults.output; } isTemplateFile(filePath) { let inputPath = this.getInputPath(filePath); // TODO use DirContains if (this.layouts && inputPath.startsWith(this.layouts)) { return false; } // if this.includes is "" (and thus is the same directory as this.input) // we don’t actually know if this is a template file, so defer if (this.includes && this.includes !== this.input) { if (inputPath.startsWith(this.includes)) { return false; } } // TODO use DirContains return inputPath.startsWith(this.input); } // for a hypothetical template file getInputPath(filePathRelativeToInputDir) { // TODO change ~/ to project root dir return TemplatePath.addLeadingDotSlash( TemplatePath.join(this.input, TemplatePath.standardizeFilePath(filePathRelativeToInputDir)), ); } // Inverse of getInputPath // Removes input dir from path getInputPathRelativeToInputDirectory(filePathRelativeToInputDir) { let inputDir = TemplatePath.addLeadingDotSlash(TemplatePath.join(this.input)); // No leading dot slash return TemplatePath.stripLeadingSubPath(filePathRelativeToInputDir, inputDir); } // for a hypothetical Eleventy layout file getLayoutPath(filePathRelativeToLayoutDir) { return TemplatePath.addLeadingDotSlash( TemplatePath.join( this.layouts || this.includes, TemplatePath.standardizeFilePath(filePathRelativeToLayoutDir), ), ); } // Removes layout dir from path getLayoutPathRelativeToInputDirectory(filePathRelativeToLayoutDir) { let layoutPath = this.getLayoutPath(filePathRelativeToLayoutDir); let inputDir = TemplatePath.addLeadingDotSlash(TemplatePath.join(this.input)); // No leading dot slash return TemplatePath.stripLeadingSubPath(layoutPath, inputDir); } getProjectPath(filePath) { return TemplatePath.addLeadingDotSlash( TemplatePath.join(".", TemplatePath.standardizeFilePath(filePath)), ); } isFileInProjectFolder(filePath) { return DirContains(TemplatePath.getWorkingDir(), filePath); } isFileInOutputFolder(filePath) { return DirContains(this.output, filePath); } static getRelativeTo(targetPath, cwd) { return path.relative(cwd, path.join(path.resolve("."), targetPath)); } // Access the data without being able to set the data. getUserspaceInstance() { let d = this; return { get input() { return d.input; }, get inputFile() { return d.inputFile; }, get inputGlob() { return d.inputGlob; }, get data() { return d.data; }, get includes() { return d.includes; }, get layouts() { return d.layouts; }, get output() { return d.output; }, }; } toString() { return { input: this.input, inputFile: this.inputFile, inputGlob: this.inputGlob, data: this.data, includes: this.includes, layouts: this.layouts, output: this.output, }; } } export default ProjectDirectories; ================================================ FILE: src/Util/ProjectTemplateFormats.js ================================================ import debugUtil from "debug"; const debug = debugUtil("Eleventy:Util:ProjectTemplateFormats"); class ProjectTemplateFormats { #useAll = {}; #raw = {}; #values = {}; // Set objects static union(...sets) { let s = new Set(); for (let set of sets) { if (!set || typeof set[Symbol.iterator] !== "function") { continue; } for (let v of set) { s.add(v); } } return s; } #normalize(formats) { if (Array.isArray(formats)) { formats = "" + formats.join(","); } if (typeof formats !== "string") { throw new Error( `Invalid formats (expect String, Array) passed to ProjectTemplateFormats->normalize: ${formats}`, ); } let final = new Set(); for (let format of formats.split(",")) { format = format.trim(); if (format && format !== "*") { final.add(format); } } return final; } isWildcard() { return this.#useAll.cli || this.#useAll.config || false; } /** @returns {boolean} */ #isUseAll(rawFormats) { if (rawFormats === "") { return false; } if (typeof rawFormats === "string") { rawFormats = rawFormats.split(","); } if (Array.isArray(rawFormats)) { return rawFormats.find((entry) => entry === "*") !== undefined; } return false; } // 3.x Breaking: "" now means no formats. In 2.x and prior it meant "*" setViaCommandLine(formats) { if (formats === undefined) { return; } this.#useAll.cli = this.#isUseAll(formats); this.#raw.cli = formats; this.#values.cli = this.#normalize(formats); } // 3.x Breaking: "" now means no formats—in 2.x and prior it meant "*" // 3.x Adds support for comma separated string—in 2.x this required an Array setViaConfig(formats) { if (formats === undefined) { return; } // "*" is supported this.#useAll.config = this.#isUseAll(formats); this.#raw.config = formats; this.#values.config = this.#normalize(formats); } addViaConfig(formats) { if (!formats) { return; } if (this.#isUseAll(formats)) { throw new Error( `\`addTemplateFormats("*")\` is not supported for project template syntaxes.`, ); } // "*" not supported here this.#raw.configAdd = formats; this.#values.configAdd = this.#normalize(formats); } getAllTemplateFormats() { return Array.from(ProjectTemplateFormats.union(this.#values.config, this.#values.configAdd)); } getTemplateFormats() { if (this.#useAll.cli) { let v = this.getAllTemplateFormats(); debug("Using CLI --formats='*': %o", v); return v; } if (this.#raw.cli !== undefined) { let v = Array.from(this.#values.cli); debug("Using CLI --formats: %o", v); return v; } let v = this.getAllTemplateFormats(); debug( "Using configuration `templateFormats`, `setTemplateFormats()`, `addTemplateFormats()`: %o", v, ); return v; } } export default ProjectTemplateFormats; ================================================ FILE: src/Util/PromiseUtil.js ================================================ export function withResolvers() { if ("withResolvers" in Promise) { return Promise.withResolvers(); } let resolve; let reject; let promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } ================================================ FILE: src/Util/Require.js ================================================ import { readFileSync } from "node:fs"; import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import importer from "./importer.js"; import { clearRequireCache, requireCommonJsTypeScript } from "../Util/RequireUtils.js"; import { port1 } from "./EsmResolverPortAdapter.js"; import EleventyBaseError from "../Errors/EleventyBaseError.js"; import eventBus from "../EventBus.js"; class EleventyImportError extends EleventyBaseError {} const requestPromiseCache = new Map(); function isCommonJSTypeScript(filePath, type) { return (type === "cjs" && filePath.endsWith(".ts")) || filePath.endsWith(".cts"); // no .mts here } function getImportErrorMessage(filePath, type) { return `There was a problem importing '${path.relative(".", filePath)}' via ${type}`; } // Used for JSON imports, suffering from Node warning that import assertions experimental but also // throwing an error if you try to import() a JSON file without an import assertion. /** * * @returns {string|undefined} */ function loadContents(path, options = {}) { let rawInput; /** @type {string} */ let encoding = "utf8"; // JSON is utf8 if (options?.encoding || options?.encoding === null) { encoding = options.encoding; } try { // @ts-expect-error This is an error in the upstream types rawInput = readFileSync(path, encoding); } catch (error) { // @ts-expect-error Temporary if (error?.code === "ENOENT") { // if file does not exist, return nothing return; } throw error; } // Can return a buffer, string, etc if (typeof rawInput === "string") { rawInput = rawInput.trim(); } return rawInput; } let lastModifiedPaths = new Map(); eventBus.on("eleventy.importCacheReset", (fileQueue) => { for (let filePath of fileQueue) { let absolutePath = TemplatePath.absolutePath(filePath); let newDate = Date.now(); lastModifiedPaths.set(absolutePath, newDate); // post to EsmResolver worker thread if (port1) { port1.postMessage({ path: absolutePath, newDate }); } clearRequireCache(absolutePath); } }); // raw means we don’t normalize away the `default` export async function dynamicImportAbsolutePath(absolutePath, options = {}) { let { type, returnRaw, cacheBust } = Object.assign( { type: undefined, returnRaw: false, cacheBust: false, // force cache bust }, options, ); // Short circuit for JSON files (that are optional and can be empty) if (absolutePath.endsWith(".json") || type === "json") { try { // https://v8.dev/features/import-assertions#dynamic-import() is still experimental in Node 20 let rawInput = loadContents(absolutePath); if (!rawInput) { // should not error when file exists but is _empty_ return; } return JSON.parse(rawInput); } catch (e) { return Promise.reject( new EleventyImportError(getImportErrorMessage(absolutePath, "fs.readFile(json)"), e), ); } } // Removed a `require` short circuit from this piece originally added // in https://github.com/11ty/eleventy/pull/3493 Was a bit faster but // error messaging was worse for require(esm) // Workaround for Node issue https://github.com/nodejs/node/issues/61385 // Remove this when fixed upstream! if (isCommonJSTypeScript(absolutePath, type)) { return requireCommonJsTypeScript(absolutePath); } let urlPath; try { let u = new URL(`file:${absolutePath}`); // Bust the import cache if this is the last modified file (or cache busting is forced) if (cacheBust) { lastModifiedPaths.set(absolutePath, Date.now()); } if (cacheBust || lastModifiedPaths.has(absolutePath)) { u.searchParams.set("_cache_bust", lastModifiedPaths.get(absolutePath)); } urlPath = u.toString(); } catch (e) { urlPath = absolutePath; } let promise; if (requestPromiseCache.has(urlPath)) { promise = requestPromiseCache.get(urlPath); } else { promise = importer(urlPath); requestPromiseCache.set(urlPath, promise); } return promise.then( (target) => { if (returnRaw) { return target; } // If the only export is `default`, elevate to top (for ESM and CJS) if (Object.keys(target).length === 1 && "default" in target) { return target.default; } // When using import() on a CommonJS file that exports an object sometimes it // returns duplicated values in `default` key, e.g. `{ default: {key: value}, key: value }` // A few examples: // module.exports = { key: false }; // returns `{ default: {key: false}, key: false }` as not expected. // module.exports = { key: true }; // module.exports = { key: null }; // module.exports = { key: undefined }; // module.exports = { key: class {} }; // A few examples where it does not duplicate: // module.exports = { key: 1 }; // returns `{ default: {key: 1} }` as expected. // module.exports = { key: "value" }; // module.exports = { key: {} }; // module.exports = { key: [] }; if (type === "cjs" && "default" in target) { let match = true; for (let key in target) { if (key === "default") { continue; } if (key === "module.exports") { continue; } if (target[key] !== target.default[key]) { match = false; } } if (match) { return target.default; } } // Otherwise return { default: value, named: value } // Object.assign here so we can add things to it in JavaScript.js return Object.assign({}, target); }, (error) => { return Promise.reject( new EleventyImportError(getImportErrorMessage(absolutePath, `import(${type})`), error), ); }, ); } async function dynamicImport(localPath, type, options = {}) { let absolutePath = TemplatePath.absolutePath(localPath); options.type = type; // Returns promise return dynamicImportAbsolutePath(absolutePath, options); } /* Used to import app configuration files, raw means we don’t normalize away the `default` export */ async function dynamicImportRaw(localPath, type) { let absolutePath = TemplatePath.absolutePath(localPath); // Returns promise return dynamicImportAbsolutePath(absolutePath, { type, returnRaw: true }); } export { loadContents as EleventyLoadContent, dynamicImport as EleventyImport, dynamicImportRaw as EleventyImportRaw, }; ================================================ FILE: src/Util/RequireUtils.core.js ================================================ import { EleventyLoadContent } from "./Require.js"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with#browser_compatibility import eleventyPackageJson from "../../package.json" with { type: "json" }; // We *could* prune everything but `name`, `version`, and `type` here but esbuild will still bundle the entire package.json export { eleventyPackageJson }; // noop export function clearRequireCache() {} // Stub for workaround for https://github.com/nodejs/node/issues/61385 export function requireCommonJsTypeScript() { throw new Error("TypeScript is not supported in this bundle of Eleventy."); } export function importJsonSync(path) { // should not be a no-op let rawInput = EleventyLoadContent(path); if (!rawInput) { // should not error when file exists but is _empty_ return; } return JSON.parse(rawInput); } ================================================ FILE: src/Util/RequireUtils.js ================================================ import { createRequire } from "node:module"; // important to clear the require.cache in CJS projects const require = createRequire(import.meta.url); export const eleventyPackageJson = require("../../package.json"); export function clearRequireCache(absolutePath) { // ESM Eleventy when using `import()` on a CJS project file still adds to require.cache if (absolutePath in (require?.cache || {})) { delete require.cache[absolutePath]; } } export function importJsonSync(filePath) { if (!filePath || !filePath.endsWith(".json")) { throw new Error(`importJsonSync expects a .json file extension (received: ${filePath})`); } return require(filePath); } export function requireCommonJsTypeScript(filePath) { return require(filePath); } ================================================ FILE: src/Util/ReservedData.js ================================================ class EleventyReservedDataError extends TypeError {} class ReservedData { static fullProperties = [ "pkg", // Object.freeze’d upstream "eleventy", // Object.freeze’d upstream // "page" is only frozen for specific subproperties below "content", "collections", ]; static properties = [ // "page" is only frozen for specific subproperties below "content", "collections", ]; static pageProperties = [ "date", "inputPath", "fileSlug", "filePathStem", "outputFileExtension", "templateSyntax", "url", "outputPath", // not yet `excerpt` or `lang` set via front matter and computed data ]; // Check in the data cascade for reserved data properties. static getReservedKeys(data, globalProperties = this.fullProperties) { if (!data) { return []; } let keys = globalProperties.filter((key) => { return key in data; }); if ("page" in data) { if (typeof data.page === "object") { for (let key of this.pageProperties) { if (key in data.page) { keys.push(`page.${key}`); } } } else { // fail `page` when set to non-object values. keys.push("page"); } } return keys; } static #check(data, sourceLocation, propertiesList) { let reservedNames = ReservedData.getReservedKeys(data, propertiesList); if (reservedNames.length === 0) { return; } throw this.getError({ reservedNames, sourceLocation, }); } // check for frozen objects too static check(data, sourceLocation) { this.#check(data, sourceLocation, this.fullProperties); } static checkSubset(data, sourceLocation) { this.#check(data, sourceLocation, this.properties); } static getError(options = {}) { let { reservedNames, cause, sourceLocation } = options || {}; if (cause) { reservedNames ??= cause.reservedNames; } let e = new EleventyReservedDataError( `You attempted to set one of Eleventy’s reserved data property names${reservedNames ? `: ${reservedNames.join(", ")}` : ""}${sourceLocation ? ` (source: ${sourceLocation})` : ""}. You can opt-out of this behavior with \`eleventyConfig.setFreezeReservedData(false)\` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. \`eleventy\`, \`pkg\`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/`, { cause }, ); if (reservedNames) { e.reservedNames = reservedNames; } return e; } static isFrozenError(e) { return ( e instanceof TypeError && e.message.startsWith("Cannot add property") && e.message.endsWith("not extensible") ); } static isReservedDataError(e) { return e instanceof EleventyReservedDataError; } } export default ReservedData; ================================================ FILE: src/Util/ResolvePlugin.client.js ================================================ export function resolvePlugin() { throw new Error( "eleventyConfig.resolvePlugin() is not supported in the `@11ty/client` bundle. You can switch to use the larger `@11ty/client/eleventy` bundle or `import` plugins directly.", ); } ================================================ FILE: src/Util/ResolvePlugin.js ================================================ import HtmlBasePlugin from "../Plugins/HtmlBasePlugin.js"; import InputPathToUrlPlugin from "../Plugins/InputPathToUrl.js"; export function resolvePlugin(name) { let filenameLookup = { // Sync, https://github.com/11ty/eleventy-plugin-rss/issues/52 "@11ty/eleventy/html-base-plugin": HtmlBasePlugin, "@11ty/eleventy/inputpath-to-url-plugin": InputPathToUrlPlugin, // Async plugins: // v4 moved RenderPlugin async for bundle size (Liquid import) "@11ty/eleventy/render-plugin": "./Plugins/RenderPlugin.js", // Liquid is ~73KB min "@11ty/eleventy/i18n-plugin": "./Plugins/I18nPlugin.js", // bcp-47-normalize is ~180KB min }; if (!filenameLookup[name]) { throw new Error( `Invalid name "${name}" passed to resolvePlugin. Valid options: ${Object.keys(filenameLookup).join(", ")}`, ); } // Future improvement: add support for any npm package name? if (typeof filenameLookup[name] === "string") { // returns promise return import(/* @vite-ignore */ filenameLookup[name]).then((plugin) => plugin.default); } // return reference return filenameLookup[name]; } ================================================ FILE: src/Util/RetrieveGlobals.client.js ================================================ export async function RetrieveGlobals(code, filePath) { let target = `data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`; return import(/* @vite-ignore */ target).then((result) => { if (Object.keys(result).length === 0) { console.warn( `Arbitrary JavaScript front matter expects the use of \`export\` when used with the \`@11ty/client\` bundle (${filePath}). Add export or swap to use the \`@11ty/client/eleventy\` bundle instead.`, ); } return result; }); } ================================================ FILE: src/Util/RetrieveGlobals.core.js ================================================ import { importFromString } from "import-module-string"; export async function RetrieveGlobals(code, filePath) { let data = { page: { // Theoretically fileSlug and filePathStem could be added here but require extensionMap inputPath: filePath, }, }; // Do *not* error when imports are found because they might be mapped via an Import Map. return importFromString(code, { data, filePath }); } ================================================ FILE: src/Util/RetrieveGlobals.js ================================================ import { RetrieveGlobals as NodeRetrieveGlobals } from "node-retrieve-globals"; import { parseCode, walkCode, importFromString } from "import-module-string"; import { isBuiltin } from "node:module"; export async function RetrieveGlobals(code, filePath, options = {}) { let { isJavaScriptFrontMatterCompat } = Object.assign( { isJavaScriptFrontMatterCompat: false }, options, ); let data = { page: { // Theoretically fileSlug and filePathStem could be added here but require extensionMap inputPath: filePath, }, }; let ast = parseCode(code); let { imports } = walkCode(ast); let nonBuiltinImports = Array.from(imports).filter((name) => !isBuiltin(name)); if (nonBuiltinImports.length === 0) { let implicitExports = isJavaScriptFrontMatterCompat ? false : true; return importFromString(code, { ast, data, filePath, implicitExports }); } // TODO re-use already parsed AST from `import-module-string` in `node-retrieve-globals` let vm = new NodeRetrieveGlobals(code, { filePath, // ignored if vm.Module is stable (or --experimental-vm-modules) transformEsmImports: true, }); // Future warning until vm.Module is stable: // If the frontMatterCode uses `import` this uses the `experimentalModuleApi` // option in node-retrieve-globals to workaround https://github.com/zachleat/node-retrieve-globals/issues/2 // this is async, but it’s handled in Eleventy upstream. return vm.getGlobalContext(data, { reuseGlobal: true, dynamicImport: true, // addRequire: true, }); } ================================================ FILE: src/Util/SemverCoerce.js ================================================ // This replaces use of semver/functions/coerce.js (which had more than we needed, we’re only targeting our local package.json for eleventy supplied global data) export function coerce(version) { let s = String(version); if (s.startsWith("v")) { s = s.slice(1); } // Remove pre-release identifier return s.split("-")[0]; } ================================================ FILE: src/Util/SetUtil.js ================================================ export function union(...sets) { let root = new Set(); for (let set of sets) { for (let entry of set) { root.add(entry); } } return root; } ================================================ FILE: src/Util/TemplateDepGraph.js ================================================ import { DepGraph as DependencyGraph } from "dependency-graph"; import debugUtil from "debug"; const debug = debugUtil("Eleventy:TemplateDepGraph"); const COLLECTION_PREFIX = "__collection:"; export class TemplateDepGraph extends DependencyGraph { static STAGES = ["[basic]", "[userconfig]", "[keys]", "all"]; #configCollectionNames = new Set(); constructor() { // BREAKING TODO move this back to non-circular with errors super({ circular: true }); let previous; // establish stage relationships, all uses keys, keys uses userconfig, userconfig uses tags for (let stageName of TemplateDepGraph.STAGES.filter(Boolean).reverse()) { let stageKey = `${COLLECTION_PREFIX}${stageName}`; if (previous) { this.uses(previous, stageKey); } previous = stageKey; } } uses(from, to) { this.addDependency(from, to); } addTag(tagName, type) { if ( tagName === "all" || (tagName.startsWith("[") && tagName.endsWith("]")) || this.#configCollectionNames.has(tagName) ) { return; } if (!type) { throw new Error( `Missing tag type for addTag. Expecting one of ${TemplateDepGraph.STAGES.map((entry) => entry.slice(1, -1)).join(" or ")}. Received: ${type}`, ); } debug("collection type %o uses tag %o", tagName, type); this.uses(`${COLLECTION_PREFIX}[${type}]`, `${COLLECTION_PREFIX}${tagName}`); } addConfigCollectionName(collectionName) { if (collectionName === "all") { return; } this.#configCollectionNames.add(collectionName); // Collection relationships to `[userconfig]` are added last, in unfilteredOrder() } cleanupCollectionNames(collectionNames = []) { let s = new Set(collectionNames); if (s.has("[userconfig]")) { return collectionNames; } let hasAnyConfigCollections = collectionNames.find((name) => { if (this.#configCollectionNames.has(name)) { return true; } return false; }); if (hasAnyConfigCollections) { s.add("[userconfig]"); } return Array.from(s); } addTemplate(filePath, consumes = [], publishesTo = []) { // Move to the beginning if it doesn’t consume anything if (consumes.length === 0) { this.uses(`${COLLECTION_PREFIX}[basic]`, filePath); } consumes = this.cleanupCollectionNames(consumes); publishesTo = this.cleanupCollectionNames(publishesTo); // Can’t consume AND publish to `all` simultaneously let consumesAll = consumes.includes("all"); if (consumesAll) { publishesTo = publishesTo.filter((entry) => entry !== "all"); } debug("%o consumes %o and publishes to %o", filePath, consumes, publishesTo); for (let collectionName of publishesTo) { if (!consumesAll) { let tagType = "basic"; let consumesUserConfigCollection = consumes.includes("[userconfig]"); if (consumesUserConfigCollection) { // must finish before [keys] tagType = "keys"; } this.addTag(collectionName, tagType); } this.uses(`${COLLECTION_PREFIX}${collectionName}`, filePath); } for (let collectionName of consumes) { this.uses(filePath, `${COLLECTION_PREFIX}${collectionName}`); let stageIndex = TemplateDepGraph.STAGES.indexOf(collectionName); let nextStage = stageIndex > 0 ? TemplateDepGraph.STAGES[stageIndex + 1] : undefined; if (nextStage) { this.uses(`${COLLECTION_PREFIX}${nextStage}`, filePath); } } } addDependency(from, to) { if (!this.hasNode(from)) { this.addNode(from); } if (!this.hasNode(to)) { this.addNode(to); } super.addDependency(from, to); } unfilteredOrder() { // these need to be added last, after the template map has been added (see addConfigCollectionName) for (let collectionName of this.#configCollectionNames) { this.uses(`${COLLECTION_PREFIX}[keys]`, `${COLLECTION_PREFIX}${collectionName}`); } return super.overallOrder(); } overallOrder() { let unfiltered = this.unfilteredOrder(); let filtered = unfiltered.filter((entry) => { if (entry === `${COLLECTION_PREFIX}[keys]`) { return true; } return !entry.startsWith(`${COLLECTION_PREFIX}[`) && !entry.endsWith("]"); }); let allKey = `${COLLECTION_PREFIX}all`; // Add another collections.all entry to the end (if not already the last one) if (filtered[filtered.length - 1] !== allKey) { filtered.push(allKey); } return filtered; } } ================================================ FILE: src/Util/TransformsUtil.js ================================================ import EleventyBaseError from "../Errors/EleventyBaseError.js"; import { isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; const debug = debugUtil("Eleventy:Transforms"); class EleventyTransformError extends EleventyBaseError {} class TransformsUtil { static changeTransformsToArray(transformsObj) { let transforms = []; for (let name in transformsObj) { transforms.push({ name: name, callback: transformsObj[name], }); } return transforms; } static async runAll(content, pageData, transforms = {}, options = {}) { let { baseHrefOverride, logger } = options; let { inputPath, outputPath, url } = pageData; if (!isPlainObject(transforms)) { throw new Error("Object of transforms expected."); } let transformsArray = this.changeTransformsToArray(transforms); for (let { callback, name } of transformsArray) { debug("Running %o transform on %o: %o", name, inputPath, outputPath); try { let hadContentBefore = !!content; content = await callback.call( { inputPath, outputPath, url, page: pageData, baseHref: baseHrefOverride, }, content, outputPath, ); if (hadContentBefore && !content) { if (!logger || !logger.warn) { throw new Error("Internal error: missing `logger` instance."); } logger.warn( `Warning: Transform \`${name}\` returned empty when writing ${outputPath} from ${inputPath}.`, ); } } catch (e) { throw new EleventyTransformError( `Transform \`${name}\` encountered an error when transforming ${inputPath}.`, e, ); } } return content; } } export default TransformsUtil; ================================================ FILE: src/Util/TypeScript/TypeScriptSample.cts ================================================ module.exports = function(b: boolean, s: string) {} ================================================ FILE: src/Util/UrlUtil.js ================================================ export function isValidUrl(url) { try { new URL(url); return true; } catch (e) { // invalid url OR local path return false; } } export function getDirectoryFromUrl(url) { if (url === false) { return false; } // returns a url if (url.endsWith("/")) { return url; } let parts = url.split("/"); parts.pop(); return parts.join("/") + "/"; } ================================================ FILE: src/Util/importer.client.js ================================================ export default function importer(relPath) { // TODO we could probably use a super streamlined version of import-module-string here that doesn’t support imports! throw new Error( "Dynamic import() is not supported in the `@11ty/client` bundle. Use the `@11ty/client/eleventy` bundle instead.", ); } ================================================ FILE: src/Util/importer.core.js ================================================ import { existsSync, readFileSync } from "node:fs"; import { importFromString } from "import-module-string"; import { fileURLToPath } from "../Adapters/Packages/url.js"; import { EleventyLoadContent } from "./Require.js"; export default function importer(relPath) { let filePath = fileURLToPath(relPath); // `import-module-string` can now `import()` so we avoid needing to esbuild these let code = EleventyLoadContent(filePath); return importFromString(code, { implicitExports: false, filePath, resolveImportContent: function (modInfo = {}) { if (modInfo.mode !== "relative") { return; } if (!existsSync(modInfo.path)) { throw new Error("Could not find content for module: " + JSON.stringify(modInfo)); } return readFileSync(modInfo.path, "utf8"); }, }); // import { parseCode, walkCode, importFromString } from "import-module-string"; // Alternative approach saved for posterity (and could be used to warn about modules needing to be Import Mapped): // let ast = parseCode(code); // let { imports } = walkCode(ast); // if(imports.size === 0) { // return importFromString(code, { ast, filePath }); // } // // This file needs to be esbuild-ed // return import(filePath); // } } ================================================ FILE: src/Util/importer.js ================================================ export default function importer(relPath) { return import(relPath); } ================================================ FILE: src/Util/spawn.core.js ================================================ export function spawnAsync() { throw new Error("This feature is not supported in `@11ty/client` bundles."); } ================================================ FILE: src/Util/spawn.js ================================================ import { spawn } from "node:child_process"; import { withResolvers } from "./PromiseUtil.js"; export function spawnAsync(command, args, options) { let { promise, resolve, reject } = withResolvers(); const cmd = spawn(command, args, options); let res = []; cmd.stdout.on("data", (data) => { res.push(data.toString("utf8")); }); let err = []; cmd.stderr.on("data", (data) => { err.push(data.toString("utf8")); }); cmd.on("close", (code) => { if (err.length > 0) { reject(err.join("\n")); } else if (code === 1) { reject("Internal error: process closed with error exit code."); } else { resolve(res.join("\n")); } }); return promise; } ================================================ FILE: src/Watch.js ================================================ import debugUtil from "debug"; import { TemplatePath } from "@11ty/eleventy-utils"; import chokidar from "chokidar"; import { isGlobMatch } from "./Util/GlobMatcher.js"; import { GlobStripper } from "./Util/GlobStripper.js"; const debug = debugUtil("Eleventy:Watch"); export class Watch { /** @type {module:chokidar} */ #chokidar; /** @type {Set} */ #watchedGlobs = []; /** @type {Set} */ #ignoredGlobs = []; constructor(config) { if (!config || config.constructor.name !== "TemplateConfig") { throw new Error("Internal error: Missing or invalid `config` argument."); } this.templateConfig = config; } getChokidarConfig() { let options = Object.assign( { ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 25, }, }, this.templateConfig.userConfig.chokidarConfig, ); // unsupported: using your own `ignored` if (options.ignored) { delete options.ignored; } return options; } // alias for watchTargets() (backwards compat) add(targets = []) { this.watchTargets(targets); } watchTargets(targets = []) { let uniqueSet = new Set(); for (let target of targets) { this.#watchedGlobs.push(TemplatePath.stripLeadingDotSlash(target)); // strip globs off of target, chokidar@4 let { path } = GlobStripper.parse(target); if (path) { uniqueSet.add(path); } } this.#chokidar?.add(Array.from(uniqueSet)); } addIgnores(ignores) { for (let target of ignores) { this.#ignoredGlobs.push(target); } } #isDirectory(path) { return this.templateConfig.existsCache.isDirectory(path); } async start() { let options = this.getChokidarConfig(); options.ignored = (filepath) => { // don’t ignore root (if specified) if (filepath === ".") { return false; } if (this.#ignoredGlobs.length > 0 && isGlobMatch(filepath, this.#ignoredGlobs)) { debug("Ignore file (ignore globs)", filepath); return true; } // don’t ignore directories that are not in ignores if (this.#isDirectory(filepath)) { return false; } // make sure this matches at least one of the original globs if (this.#watchedGlobs.length === 0 || !isGlobMatch(filepath, this.#watchedGlobs)) { debug("Ignore file (no glob match)", filepath, this.#watchedGlobs); return true; } return false; }; // strip globs off of target, chokidar@4 let targets = this.#watchedGlobs .map((target) => { let { path } = GlobStripper.parse(target); return path; }) .filter(Boolean); this.#chokidar = chokidar.watch(targets, options); // Note: if there are no watch targets the `ready` event doesn’t fire so skip it if (targets.length > 0) { await new Promise((resolve) => { this.#chokidar.on("ready", () => resolve()); }); } } on(event, callback) { this.#chokidar.on(event, callback); } async close() { return this.#chokidar?.close(); } } ================================================ FILE: src/WatchQueue.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import PathNormalizer from "./Util/PathNormalizer.js"; /* Decides when to watch and in what mode to watch * Incremental builds don’t batch changes, they queue. * Nonincremental builds batch. */ class WatchQueue { constructor() { this.incremental = false; this.isActive = false; this.activeQueue = []; } isBuildRunning() { return this.isActive; } setBuildRunning() { this.isActive = true; // pop waiting queue into the active queue this.activeQueue = this.popNextActiveQueue(); } setBuildFinished() { this.isActive = false; this.activeQueue = []; } getIncrementalFile() { if (this.incremental) { return this.activeQueue.length ? this.activeQueue[0] : false; } return false; } /* Returns the changed files currently being operated on in the current `watch` build * Works with or without incremental (though in incremental only one file per time will be processed) */ getActiveQueue() { if (!this.isActive) { return []; } else if (this.incremental && this.activeQueue.length === 0) { return []; } else if (this.incremental) { return [this.activeQueue[0]]; } return this.activeQueue; } _queueMatches(file) { let filterCallback; if (typeof file === "function") { filterCallback = file; } else { filterCallback = (path) => path === file; } return this.activeQueue.filter(filterCallback); } hasAllQueueFiles(file) { return ( this.activeQueue.length > 0 && this.activeQueue.length === this._queueMatches(file).length ); } hasQueuedFile(file) { if (file) { return this._queueMatches(file).length > 0; } return false; } hasQueuedFiles(files) { for (const file of files) { if (this.hasQueuedFile(file)) { return true; } } return false; } get pendingQueue() { if (!this._queue) { this._queue = []; } return this._queue; } set pendingQueue(value) { this._queue = value; } addToPendingQueue(path) { if (path) { path = PathNormalizer.normalizeSeperator(TemplatePath.addLeadingDotSlash(path)); this.pendingQueue.push(path); } } getPendingQueueSize() { return this.pendingQueue.length; } getPendingQueue() { return this.pendingQueue; } getActiveQueueSize() { return this.activeQueue.length; } // returns array popNextActiveQueue() { if (this.incremental) { return this.pendingQueue.length ? [this.pendingQueue.shift()] : []; } let ret = this.pendingQueue.slice(); this.pendingQueue = []; return ret; } } export default WatchQueue; ================================================ FILE: src/WatchTargets.js ================================================ import { TemplatePath } from "@11ty/eleventy-utils"; import { DepGraph } from "dependency-graph"; import { mergeGraphs } from "@11ty/dependency-tree-esm"; import JavaScriptDependencies from "./Util/JavaScriptDependencies.js"; import eventBus from "./EventBus.js"; export default class WatchTargets { #templateConfig; constructor(templateConfig) { this.targets = new Set(); this.dependencies = new Set(); this.newTargets = new Set(); this.isEsm = false; this.graph = new DepGraph(); this.#templateConfig = templateConfig; } setProjectUsingEsm(isEsmProject) { this.isEsm = !!isEsmProject; } isJavaScriptDependency(path) { return this.dependencies.has(path); } reset() { this.newTargets = new Set(); } addToDependencyGraph(parent, deps) { if (!this.graph.hasNode(parent)) { this.graph.addNode(parent); } for (let dep of deps) { if (!this.graph.hasNode(dep)) { this.graph.addNode(dep); } this.graph.addDependency(parent, dep); } } uses(parent, dep) { return this.getDependenciesOf(parent).includes(dep); } getDependenciesOf(parent) { if (!this.graph.hasNode(parent)) { return []; } return this.graph.dependenciesOf(parent); } getDependantsOf(child) { if (!this.graph.hasNode(child)) { return []; } return this.graph.dependantsOf(child); } addRaw(targets, isDependency) { for (let target of targets) { let path = TemplatePath.addLeadingDotSlash(target); if (!this.targets.has(target)) { this.newTargets.add(path); } this.targets.add(path); if (isDependency) { this.dependencies.add(path); } } } static toArray(targets) { if (!targets) { return []; } else if (Array.isArray(targets)) { return targets; } return [targets]; } // add only a target add(targets) { this.addRaw(WatchTargets.toArray(targets)); } static normalizeToGlobs(targets) { return WatchTargets.toArray(targets).map((entry) => TemplatePath.convertToRecursiveGlobSync(entry), ); } // add only a target’s dependencies async addDependencies(targets, filterCallback) { if (this.#templateConfig && !this.#templateConfig.shouldSpiderJavaScriptDependencies()) { return; } targets = WatchTargets.toArray(targets); let cjsDeps = Array.from( await JavaScriptDependencies.getCommonJsDependencies(targets, this.isEsm), ); if (filterCallback) { cjsDeps = cjsDeps.filter(filterCallback); } for (let target of targets) { this.addToDependencyGraph(target, cjsDeps); } this.addRaw(cjsDeps, true); // https://github.com/11ty/eleventy/issues/3899 // Note that this fix is ESM-only, dependency-tree CJS doesn’t support returning graphs (yet?) let esmGraph = await JavaScriptDependencies.getEsmGraph(targets, this.isEsm); if (filterCallback) { for (let node of esmGraph.overallOrder()) { if (!filterCallback(node)) { esmGraph.removeNode(node); } } } mergeGraphs(this.graph, esmGraph); // ESM graph includes original targets, which we do not want for addRaw so we’ll remove them before adding let rawEsmGraph = esmGraph.clone(); for (let t of targets) { rawEsmGraph.removeNode(t); } this.addRaw(rawEsmGraph.overallOrder(), true); } setWriter(templateWriter) { this.writer = templateWriter; } clearImportCacheFor(filePathArray) { let paths = new Set(); for (const filePath of filePathArray) { paths.add(filePath); // Delete from require cache so that updates to the module are re-required let importsTheChangedFile = this.getDependantsOf(filePath); for (let dep of importsTheChangedFile) { paths.add(dep); } let isImportedInTheChangedFile = this.getDependenciesOf(filePath); for (let dep of isImportedInTheChangedFile) { paths.add(dep); } // Use GlobalDependencyMap let dependantsMapped = this.#templateConfig?.usesGraph.getDependantsFor(filePath) || []; for (let dep of dependantsMapped) { paths.add(dep); } } eventBus.emit("eleventy.importCacheReset", paths); } getNewTargetsSinceLastReset() { return Array.from(this.newTargets); } getTargets() { return Array.from(this.targets); } } ================================================ FILE: src/defaultConfig.js ================================================ import fullBundleDefaultConfig from "./defaultConfigExtended.js"; import TransformsUtil from "./Util/TransformsUtil.js"; /** * @module 11ty/eleventy/defaultConfig */ /** * @callback addFilter - Register a global filter. * @param {string} name - Register a template filter by this name. * @param {function} callback - The filter logic. */ /** * @typedef {object} config * @property {addFilter} addFilter - Register a new global filter. * @property {addPlugin} addPlugin - Execute or defer a plugin’s execution. * @property {addTransform} addTransform - Add an Eleventy transform to postprocess template output */ /** * @typedef {object} defaultConfig * @property {Array} templateFormats - An array of accepted template formats. * @property {Array} dataFileSuffixes - Array of file suffixes for data files in the Data Cascade. * @property {boolean} [dataFileDirBaseNameOverride=false] - Use index.* instead of dirname.* for Directory Data File names * @property {string} [pathPrefix='/'] - The directory under which all output files should be written to. * @property {string} [markdownTemplateEngine='liquid'] - Template engine to process markdown files with. * @property {string} [htmlTemplateEngine='liquid'] - Template engine to process html files with. * @property {boolean} [dataTemplateEngine=false] - Changed in v1.0 * @property {string} [jsDataFileSuffix='.11tydata'] - File suffix for jsData files. * @property {object} keys * @property {string} [keys.package='pkg'] - Global data property for package.json data * @property {string} [keys.layout='layout'] * @property {string} [keys.permalink='permalink'] * @property {string} [keys.permalinkRoot='permalinkBypassOutputDir'] * @property {string} [keys.engineOverride='templateEngineOverride'] * @property {string} [keys.computed='eleventyComputed'] * @property {string} [keys.dataSchema='eleventyDataSchema'] * @property {object} dir * @property {string} [dir.input='.'] * @property {string} [dir.includes='_includes'] * @property {string} [dir.data='_data'] * @property {string} [dir.output='_site'] * @deprecated handlebarsHelpers * @deprecated nunjucksFilters */ /** * Default configuration object factory. * * @param {config} config - Eleventy configuration object. * @returns {defaultConfig} */ export default function (config) { // add extra config (not available in `@11ty/client` bundle) fullBundleDefaultConfig.call(this, config); config.addFilter("log", (input, ...messages) => { console.log(input, ...messages); return input; }); // Process arbitrary content with transforms config.addFilter( "renderTransforms", async function transformsFilter(content, pageEntryOverride, baseHrefOverride) { return TransformsUtil.runAll(content, pageEntryOverride || this.page, config.transforms, { baseHrefOverride, logger: config.logger, }); }, ); return { templateFormats: ["liquid", "md", "njk", "html", "11ty.js"], // to add a parent directory structure to URLs (not reflected on the file system), change this pathPrefix: "/", markdownTemplateEngine: "liquid", htmlTemplateEngine: "liquid", // Renamed from `jsDataFileSuffix` in 2.0 (and swapped to an Array) // If you remove "" we won’t look for dir/dir.json or file.json dataFileSuffixes: [".11tydata", ""], // "index" will look for `directory/index.*` directory data files instead of `directory/directory.*` dataFileDirBaseNameOverride: false, keys: { // TODO breaking: use `false` by default package: "pkg", // supports `false` layout: "layout", permalink: "permalink", permalinkRoot: "permalinkBypassOutputDir", engineOverride: "templateEngineOverride", computed: "eleventyComputed", dataSchema: "eleventyDataSchema", }, // Deprecated, define using `export const directories = {}` instead. // Reference values using `eleventyConfig.directories` instead. dir: { // These values here aren’t used internally either (except by a few tests), instead we’re using `ProjectDirectories.defaults`. // These are kept in place for backwards compat with `eleventyConfig.dir` references in project config code and plugins. input: ".", includes: "_includes", data: "_data", output: "_site", }, // deprecated, use config.addNunjucksFilter nunjucksFilters: {}, }; } ================================================ FILE: src/defaultConfigExtended.client.js ================================================ export default function (config) { config.addFilter("url", () => { throw new Error( "The `url` filter is not included with the `@11ty/client` bundle. Use the `@11ty/client/eleventy` bundle.", ); }); config.addFilter("inputPathToUrl", () => { throw new Error( "The `inputPathToUrl` filter is not included with the `@11ty/client` bundle. Use the larger `@11ty/client/eleventy` bundle.", ); }); // Saves ~26KB (minified) // Differences from main bundle: async and not memoized config.addAsyncFilter("slugify", async function (str, options = {}) { return import("@sindresorhus/slugify") .then((mod) => mod.default) .then((slugify) => { options.decamelize ??= false; return slugify("" + str, options); }); }); } ================================================ FILE: src/defaultConfigExtended.js ================================================ import bundlePlugin from "@11ty/eleventy-plugin-bundle"; import slugify from "@sindresorhus/slugify"; import { HtmlTransformer } from "./Util/HtmlTransformer.js"; import { HtmlRelativeCopyPlugin } from "./Plugins/HtmlRelativeCopyPlugin.js"; import MemoizeUtil from "./Util/MemoizeFunction.js"; import urlFilter from "./Filters/Url.js"; import getLocaleCollectionItem from "./Filters/GetLocaleCollectionItem.js"; import getCollectionItemIndex from "./Filters/GetCollectionItemIndex.js"; import { FilterPlugin as InputPathToUrlFilterPlugin } from "./Plugins/InputPathToUrl.js"; /** * @typedef {object} config * @property {addPlugin} addPlugin - Execute or defer a plugin’s execution. * @property {addTransform} addTransform - Add an Eleventy transform to postprocess template output * @property {htmlTransformer} htmlTransformer - HTML modification API */ /** * Extended default configuration object factory. * * @param {config} config - Eleventy configuration object. * @returns {defaultConfig} */ export default function (config) { // Used for the HTML , InputPathToUrl, Image transform plugins let htmlTransformer = new HtmlTransformer(); htmlTransformer.setUserConfig(config); // This needs to be assigned before bundlePlugin is added below. config.htmlTransformer = htmlTransformer; // Remember: the transform added here runs before the `htmlTransformer` transform config.addPlugin(bundlePlugin, { bundles: false, // no default bundles included—must be opt-in. immediate: true, }); // Run the `htmlTransformer` transform config.addTransform("@11ty/eleventy/html-transformer", async function (content) { // Runs **AFTER** the bundle plugin transform (except: delayed bundles) return htmlTransformer.transformContent(this.outputPath, content, this); }); // Requires user configuration, so must run as second-stage config.addPlugin(HtmlRelativeCopyPlugin); // Filter: Maps an input path to output URL config.addPlugin(InputPathToUrlFilterPlugin, { immediate: true, }); // slug Filter (removed, errors) config.addFilter("slug", function () { throw new Error( "The `slug` filter (deprecated since v1) has been removed in Eleventy v4. You can add it manually to your configuration file for backwards compatibility, read more at GitHub Issue #3893: https://github.com/11ty/eleventy/issues/3893 Alternatively (more risky), you can swap to use the `slugify` filter instead (outputs may be different and production URLs may break!)", ); }); // slugify Filter config.addFilter( "slugify", MemoizeUtil( function (str, options = {}) { options.decamelize ??= false; return slugify("" + str, options); }, { name: "slugify", bench: config.benchmarkManager.get("Configuration") }, ), ); // Collection Filters config.addFilter("getCollectionItemIndex", function (collection, pageOverride) { return getCollectionItemIndex.call(this, collection, pageOverride); }); config.addFilter("getCollectionItem", function (collection, pageOverride, langCode) { return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, 0); }); config.addFilter("getPreviousCollectionItem", function (collection, pageOverride, langCode) { return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, -1); }); config.addFilter("getNextCollectionItem", function (collection, pageOverride, langCode) { return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, 1); }); // Deprecated, use HtmlBasePlugin instead. // Adds a pathPrefix manually to a URL string let templateConfig = this; config.addFilter("url", function addPathPrefixFilter(url, pathPrefixOverride) { let pathPrefix; if (pathPrefixOverride && typeof pathPrefixOverride === "string") { pathPrefix = pathPrefixOverride; } else { pathPrefix = templateConfig.getPathPrefix(); } return urlFilter.call(this, url, pathPrefix); }); } ================================================ FILE: test/ArrayUtilTest.js ================================================ import test from "ava"; import {arrayDelete} from "../src/Util/ArrayUtil.js"; test("ArrayUtil.arrayDelete empties", async (t) => { t.deepEqual(arrayDelete(), []); t.deepEqual(arrayDelete(undefined, 1), []); t.deepEqual(arrayDelete(null), []); t.deepEqual(arrayDelete(1), []); t.deepEqual(arrayDelete(true), []); t.deepEqual(arrayDelete(false), []); }); test("ArrayUtil.arrayDelete if array does not have value, it does not mutate", async (t) => { let empty = []; t.is(arrayDelete(empty), empty); t.is(arrayDelete(empty, 1), empty); t.is(arrayDelete(empty, true), empty); t.is(arrayDelete(empty, undefined), empty); }); test("ArrayUtil.arrayDelete if array does not have function matched value, it does not mutate", async (t) => { let empty = []; t.is(arrayDelete(empty, () => false), empty); }); test("ArrayUtil.arrayDelete mutates when array contains match", async (t) => { let a = [1, 2]; t.not(arrayDelete(a, 1), [2]); t.deepEqual(arrayDelete(a, 1), [2]); }); test("ArrayUtil.arrayDelete mutates when array contains function matched value", async (t) => { let a = [1, 2]; t.not(arrayDelete(a, entry => entry === 1), [2]); t.deepEqual(arrayDelete(a, entry => entry === 1), [2]); }); test("ArrayUtil.arrayDelete complex delete", async (t) => { let a = [1,2,3,4,5,6,7,8]; t.deepEqual(arrayDelete(a, 4), [1,2,3,5,6,7,8]); }); test("ArrayUtil.arrayDelete function matched delete", async (t) => { let a = [1,2,3,4,5,6,7,8]; t.deepEqual(arrayDelete(a, entry => entry === 4), [1,2,3,5,6,7,8]); }); test("ArrayUtil.arrayDelete double delete", async (t) => { let a = [1,2,3,4,5,6,7,8]; t.deepEqual(arrayDelete(arrayDelete(a, 4), 6), [1,2,3,5,7,8]); }); ================================================ FILE: test/BenchmarkTest.js ================================================ import test from "ava"; import Benchmark from "../src/Benchmark/Benchmark.js"; test("Standard Benchmark", async (t) => { await new Promise((resolve) => { let b = new Benchmark(); b.before(); setTimeout(function () { b.after(); t.truthy(b.getTotal() >= 0); resolve(); }, 100); }); }); test("Nested Benchmark (nested calls are ignored while a parent is measuring)", async (t) => { await new Promise((resolve) => { let b = new Benchmark(); b.before(); setTimeout(function () { b.before(); b.after(); t.truthy(b.getTotal() <= 0.1); b.after(); t.truthy(b.getTotal() >= 10); resolve(); }, 100); }); }); test("Reset Benchmark", async (t) => { await new Promise((resolve) => { let b = new Benchmark(); b.before(); b.reset(); setTimeout(function () { b.before(); b.after(); t.throws(function () { // throws because we reset b.after(); }); resolve(); }, 100); }); }); ================================================ FILE: test/BundlePluginTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("addBundle", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css") }); eleventyConfig.addTemplate("index.njk", "{% css %}/* Hi */{% endcss %}"); } }); let results = await elev.toJSON(); t.is(results[0].content, ``); }); test("addBundle (empty css)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("addBundle (empty js)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("js"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("Empty script node is removed (not using bundle)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("js"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("Empty style node is removed (not using bundle)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("Empty link node is removed (not using bundle)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("Empty link node is removed (no href attribute at all, not using bundle)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); test("Empty link node is kept (no rel attribute, not using bundle)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPlugin(() => { eleventyConfig.addBundle("css"); }); eleventyConfig.addTemplate("index.njk", "Hi"); } }); let results = await elev.toJSON(); t.is(results[0].content, `Hi`); }); ================================================ FILE: test/CompatibilityTest.js ================================================ import test from "ava"; import EleventyCompatibility from "../src/Util/Compatibility.js"; test(".canary- to .alpha- normalization (because pre-releases are alphabetic comparisons 😭)", (t) => { t.is(EleventyCompatibility.normalizeIdentifier("2.0.0"), "2.0.0"); t.is(EleventyCompatibility.normalizeIdentifier("2.0.0-beta.1"), "2.0.0-beta.1"); t.is(EleventyCompatibility.normalizeIdentifier("2.0.0-canary.1"), "2.0.0-alpha.1"); t.is(EleventyCompatibility.normalizeIdentifier("2.0.0-alpha.1"), "2.0.0-alpha.1"); t.is(EleventyCompatibility.normalizeIdentifier(">=2.0.0-beta.1"), ">=2.0.0-beta.1"); t.is( EleventyCompatibility.normalizeIdentifier(">=2.0.0-beta.1 || >=2.0.0-canary.1"), ">=2.0.0-beta.1 || >=2.0.0-alpha.1" ); }); test("Version checking for plugin compatibility >=0.5.4", (t) => { let range = ">=0.5.4"; t.true(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=0.6", (t) => { let range = ">=0.6"; t.true(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=1", (t) => { let range = ">=1"; // **not** the same as >=1.0.0 t.true(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=1.0.0", (t) => { let range = ">=1.0.0"; // **not** the same as >=1 t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=1.0.0 || >=1.0.0-beta || >=1.0.0-canary", (t) => { // could be simplified to >=1 // noting that pre-1.0 versions of Eleventy did not match prereleases—which doesn’t matter because 0.12 would return false here anyway. let range = ">=1.0.0 || >=1.0.0-beta || >=1.0.0-canary"; t.true(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2", (t) => { let range = ">=2"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0", (t) => { // Recommend to use >=2 instead of this let range = ">=2.0.0"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); // Contentious! I wish this were true t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); // Contentious! I wish this were true t.false(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); // Contentious! I wish this were true t.false(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); // Contentious! I wish this were true t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-canary", (t) => { // Recommend to use >=2 instead of this let range = ">=2.0.0-canary"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-canary.1", (t) => { // Recommend to use >=2 instead of this let range = ">=2.0.0-canary.1"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.3", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-beta", (t) => { let range = ">=2.0.0-beta"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-beta.1", (t) => { let range = ">=2.0.0-beta.1"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-beta.2", (t) => { let range = ">=2.0.0-beta.2"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.false(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); // TODO eleventy-upgrade-help test("Version checking for plugin compatibility >=2 <3", (t) => { let range = ">=2 <3"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.false(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.false(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-canary.19", (t) => { let range = ">=2.0.0-canary.19"; t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-canary.19 || >=2.0.0-beta.1", (t) => { let range = ">=2.0.0-canary.19 || >=2.0.0-beta.1"; // can be simplified to >=2.0.0-canary.19 t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); test("Version checking for plugin compatibility >=2.0.0-canary.19 || >=2.0.0-beta.2", (t) => { let range = ">=2.0.0-canary.19 || >=2.0.0-beta.2"; // same as ">=2.0.0-canary.19" t.false(EleventyCompatibility.satisfies("1.0.0-beta.1", range)); t.false(EleventyCompatibility.satisfies("1.0.2", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary", range)); t.false(EleventyCompatibility.satisfies("2.0.0-canary.18", range)); t.true(EleventyCompatibility.satisfies("2.0.0-canary.19", range)); t.true(EleventyCompatibility.satisfies("2.0.0-beta.1", range)); // warning: this matches because of >=2.0.0-canary.19 t.true(EleventyCompatibility.satisfies("2.0.0-beta.2", range)); t.true(EleventyCompatibility.satisfies("2.0.0", range)); t.true(EleventyCompatibility.satisfies("2.0.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-canary.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0-beta.1", range)); t.true(EleventyCompatibility.satisfies("3.0.0", range)); }); ================================================ FILE: test/ComputedDataProxyTest.js ================================================ import test from "ava"; import ComputedDataProxy from "../src/Data/ComputedDataProxy.js"; test("Get vars used by function", async (t) => { let cd = new ComputedDataProxy(["key1"]); let key1Fn = () => {}; let key2Fn = (data) => { return `${data.key1}`; }; t.deepEqual(await cd.findVarsUsed(key1Fn), []); t.deepEqual(await cd.findVarsUsed(key2Fn), ["key1"]); }); test("Get vars used by function (not a computed key)", async (t) => { let cd = new ComputedDataProxy(["page.url"]); let key1Fn = (data) => { return `${data.page.url}`; }; t.deepEqual( await cd.findVarsUsed(key1Fn, { page: { url: "" }, }), ["page.url"] ); }); test("Get vars used by function (multiple functions—not computed keys)", async (t) => { let cd = new ComputedDataProxy([ "page.url", "key1", "very.deep.reference", "very.other.deep.reference", ]); // this would be real let sampleData = { key1: "", page: { url: "", }, very: { deep: { reference: "", }, other: { deep: { reference: "", }, }, }, }; let key1Fn = (data) => { return `${data.page.url}`; }; let key2Fn = (data) => { return `${data.key1}${data.very.deep.reference}${data.very.other.deep.reference}`; }; t.deepEqual(await cd.findVarsUsed(key1Fn, sampleData), ["page.url"]); t.deepEqual(await cd.findVarsUsed(key2Fn, sampleData), [ "key1", "very.deep.reference", "very.other.deep.reference", ]); }); test("Proxy shouldn’t always return {}", async (t) => { let cd = new ComputedDataProxy(["page.fileSlug"]); let proxy = cd.getProxyData( { page: { fileSlug: "", }, }, new Set() ); t.notDeepEqual(proxy.page.fileSlug, {}); t.is(proxy.page.fileSlug, ""); }); test("isArrayOrPlainObject", async (t) => { let cd = new ComputedDataProxy(); t.is(cd.isArrayOrPlainObject(true), false); t.is(cd.isArrayOrPlainObject(false), false); t.is(cd.isArrayOrPlainObject(1), false); t.is( cd.isArrayOrPlainObject(() => {}), false ); t.is( cd.isArrayOrPlainObject(function () {}), false ); t.is(cd.isArrayOrPlainObject(new Date()), false); t.is(cd.isArrayOrPlainObject({}), true); t.is(cd.isArrayOrPlainObject([]), true); }); test("findVarsUsed empty", async (t) => { let cdg = new ComputedDataProxy(); t.deepEqual(await cdg.findVarsUsed(() => {}), []); t.deepEqual(await cdg.findVarsUsed(({}) => {}), []); let data = { key: "value" }; t.deepEqual(await cdg.findVarsUsed((data) => {}), []); t.deepEqual(await cdg.findVarsUsed((data) => data.key), ["key"]); }); test("findVarsUsed with a computed key (target a string)", async (t) => { let cdg = new ComputedDataProxy(); let data = { key: "value", computed: { key: function (data) { return data.key; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["key"]); }); test("findVarsUsed with a computed key (target an array)", async (t) => { let cdg = new ComputedDataProxy(); let data = { arr: [0, 1, 2], computed: { key: function (data) { return data.arr[1]; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["arr[1]"]); }); test("findVarsUsed with a computed key (target an object)", async (t) => { let cdg = new ComputedDataProxy(); let data = { obj: { b: 1, }, computed: { key: function (data) { return data.obj.b; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["obj.b"]); }); test("findVarsUsed with a computed key (target an object in an array)", async (t) => { let cdg = new ComputedDataProxy(); let data = { obj: [{ b: 1 }, { a: 2 }], computed: { key: function (data) { return data.obj[1].a; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["obj[1].a"]); }); test("findVarsUsed with a computed key (target a string not used in the output)", async (t) => { let cdg = new ComputedDataProxy(); let data = { key1: "value1", key2: "value2", computed: { key: function (data) { let b = data.key2; return data.key1; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["key2", "key1"]); }); test("findVarsUsed with a deep computed reference that doesn’t exist in parent data", async (t) => { let cdg = new ComputedDataProxy(["deep.deep1", "deep.deep2"]); let data = { key1: "value1", key2: "value2", computed: { deep: { deep1: function (data) { return data.key2; }, deep2: function (data) { return data.deep.deep1; }, }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.deep.deep2, data), ["deep.deep1"]); t.deepEqual(await cdg.findVarsUsed(data.computed.deep.deep1, data), ["key2"]); }); test("findVarsUsed with a array should filter out array methods", async (t) => { let cdg = new ComputedDataProxy(); let data = { arr: [0, 1, 2], computed: { key: function (data) { return data.arr.filter((entry) => entry === 2); }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["arr"]); }); test("findVarsUsed with a array can still reference length", async (t) => { let cdg = new ComputedDataProxy(); let data = { arr: [0, 1, 2], computed: { key: function (data) { return data.arr.length; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["arr"]); }); test("findVarsUsed can work with empty arrays", async (t) => { let cdg = new ComputedDataProxy(); let data = { arr: [], computed: { key: function (data) { return data.arr; }, }, }; t.deepEqual(await cdg.findVarsUsed(data.computed.key, data), ["arr"]); }); ================================================ FILE: test/ComputedDataQueueTest.js ================================================ import test from "ava"; import ComputedDataQueue from "../src/Data/ComputedDataQueue.js"; test("Standard uses", (t) => { let queue = new ComputedDataQueue(); queue.uses("permalink", ["var1", "var2"]); queue.uses("collections.all", ["var2", "var3"]); t.deepEqual(queue.getOrder(), ["var1", "var2", "permalink", "var3", "collections.all"]); }); test("What does permalink use", (t) => { let queue = new ComputedDataQueue(); queue.uses("permalink", ["var1", "var2"]); queue.uses("collections.all", ["var2", "var3"]); let varsUsedByPermalink = queue.getOrderFor("permalink"); t.deepEqual(varsUsedByPermalink, ["var1", "var2"]); // After we process these queue.markComputed(["permalink", ...varsUsedByPermalink]); t.deepEqual(queue.getOrder(), ["var3", "collections.all"]); }); test("What does page.url and page.outputPath use", (t) => { let queue = new ComputedDataQueue(); queue.uses("page.url", ["permalink"]); queue.uses("page.url", ["var1", "var2"]); queue.uses("page.outputPath", ["permalink"]); queue.uses("page.outputPath", ["var2", "var3"]); let varsUsedByPageUrl = queue.getOrderFor("page.url"); t.deepEqual(varsUsedByPageUrl, ["permalink", "var1", "var2"]); queue.markComputed([...varsUsedByPageUrl, "page.url"]); t.deepEqual(queue.getOrder(), ["var3", "page.outputPath"]); let varsUsedByPageOutput = queue.getOrderFor("page.outputPath"); // even though page.outputPath used permalink and var2, // they were already computed above by page.url t.deepEqual(varsUsedByPageOutput, ["var3"]); queue.markComputed([...varsUsedByPageOutput, "page.outputPath"]); t.deepEqual(queue.getOrder(), []); }); test("Permalink uses a collection (not yet supported in Eleventy)", (t) => { let queue = new ComputedDataQueue(); queue.uses("permalink", ["collections.dog", "var2"]); queue.uses("collections.all", ["var2", "var3"]); queue.uses("collections.dog", ["hi"]); queue.uses("unrelated", ["test"]); t.deepEqual(queue.getDependsOn("collections.dog"), ["permalink"]); t.deepEqual(queue.getDependsOn("var2"), ["permalink", "collections.all"]); t.deepEqual(queue.getDependsOn("collections.all"), []); t.deepEqual(queue.getDependsOn("hi"), ["permalink", "collections.dog"]); t.is(queue.isUsesStartsWith("collections.dog", "hi"), true); t.is(queue.isUsesStartsWith("permalink", "collections."), true); t.is(queue.isUsesStartsWith("unrelated", "collections."), false); t.deepEqual(queue.getOrderFor("unrelated"), ["test"]); let varsUsedByPermalink = queue.getOrderFor("permalink"); t.deepEqual(varsUsedByPermalink, ["hi", "collections.dog", "var2"]); // After we process these queue.markComputed(["permalink", ...varsUsedByPermalink]); t.deepEqual(queue.getOrder(), ["var3", "collections.all", "test", "unrelated"]); }); ================================================ FILE: test/ComputedDataTemplateStringTest.js ================================================ import test from "ava"; import ComputedDataTemplateString from "../src/Data/ComputedDataTemplateString.js"; test("Get fake proxy data", (t) => { let cd = new ComputedDataTemplateString(["key1", "key2"]); t.deepEqual(cd.getProxyData(), { key1: `${cd.prefix}key1${cd.suffix}`, key2: `${cd.prefix}key2${cd.suffix}`, }); }); test("Get nested fake proxy data", (t) => { let cd = new ComputedDataTemplateString(["key1.nested", "key2"]); t.deepEqual(cd.getProxyData(), { key1: { nested: `${cd.prefix}key1.nested${cd.suffix}`, }, key2: `${cd.prefix}key2${cd.suffix}`, }); }); test("Get vars from output", (t) => { let cd = new ComputedDataTemplateString(); t.deepEqual(cd.findVarsInOutput(""), []); t.deepEqual(cd.findVarsInOutput("slkdjfkljdsf"), []); t.deepEqual(cd.findVarsInOutput(`slkdjfkljdsf${cd.prefix}${cd.suffix}sldkjflkds`), []); t.deepEqual(cd.findVarsInOutput(`slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}sldkjflkds`), [ "firstVar", ]); t.deepEqual( cd.findVarsInOutput( `slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}test${cd.prefix}firstVar${cd.suffix}sldkjflkds` ), ["firstVar"] ); t.deepEqual( cd.findVarsInOutput( `slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}test${cd.prefix}secondVar${cd.suffix}sldkjflkds` ), ["firstVar", "secondVar"] ); }); ================================================ FILE: test/ComputedDataTest.js ================================================ import test from "ava"; import ComputedData from "../src/Data/ComputedData.js"; import TemplateConfig from "../src/TemplateConfig.js"; test("Basic get/set", async (t) => { let cd = new ComputedData(); cd.add("keystr", "this is a str"); cd.add("key1", (data) => { return `this is a test ${data.key2}${data.keystr}`; }); let data = { key2: "inject me", }; await cd.setupData(data); t.is(data.key1, "this is a test inject methis is a str"); t.is(data.key2, "inject me"); t.is(data.keystr, "this is a str"); }); test("Basic get/set (reverse order of adds)", async (t) => { let cd = new ComputedData(); cd.add("key1", (data) => { return `this is a test ${data.key2}${data.keystr}`; }); cd.add("keystr", "this is a str"); let data = { key2: "inject me", }; await cd.setupData(data); t.is(data.key1, "this is a test inject methis is a str"); t.is(data.key2, "inject me"); t.is(data.keystr, "this is a str"); }); test("Basic get/set (reverse order of adds) nested two deep", async (t) => { let cd = new ComputedData(); cd.add("key1.key3", (data) => { return `this is a test ${data.key2}${data.keystr}`; }); cd.add("key1.key4", (data) => { return `this is a test ${data.key1.key3}`; }); cd.add("keystr", "this is a str"); let data = { key2: "inject me", }; await cd.setupData(data); t.is(data.key1.key3, "this is a test inject methis is a str"); t.is(data.key1.key4, "this is a test this is a test inject methis is a str"); t.is(data.key2, "inject me"); t.is(data.keystr, "this is a str"); }); test("use a computed value in another computed", async (t) => { let cd = new ComputedData(); cd.add("keyComputed", (data) => { return `this is a test ${data.keyOriginal}`; }); cd.add("keyComputed2nd", (data) => { return `using computed ${data.keyComputed}`; }); let data = { keyOriginal: "inject me", }; await cd.setupData(data); t.is(data.keyComputed2nd, "using computed this is a test inject me"); }); test("use a computed value in another computed (out of order)", async (t) => { let cd = new ComputedData(); cd.add("keyComputed2nd", (data) => { return `using computed ${data.keyComputed}`; }); cd.add("keyComputed", (data) => { return `this is a test ${data.keyOriginal}`; }); let data = { keyOriginal: "inject me", }; await cd.setupData(data); t.is(data.keyComputed2nd, "using computed this is a test inject me"); }); test("use a computed value in another computed (out of order), async callbacks", async (t) => { let cd = new ComputedData(); cd.add("keyComputed2nd", async (data) => { // await in data.keyComputed is optional 👀 return `using computed ${data.keyComputed}`; }); cd.add("keyComputed", async (data) => { // await in data.keyOriginal is optional 👀 return `this is a test ${await data.keyOriginal}`; }); let data = { keyOriginal: "inject me", }; await cd.setupData(data); t.is(data.keyComputed2nd, "using computed this is a test inject me"); }); test("Basic get/set nested", async (t) => { let cd = new ComputedData(); cd.add("key1.nested", (data) => { return `${data.key2}`; }); cd.add("key2", (data) => "hi"); let data = { key2: "inject me", }; await cd.setupData(data); t.deepEqual(data.key1, { nested: "hi" }); t.is(data.key1.nested, "hi"); t.is(data.key2, "hi"); }); test("Basic get/set nested deeper", async (t) => { let cd = new ComputedData(); cd.add("key1.nested.deeperA", (data) => { return `${data.key2}`; }); cd.add("key1.nested.deeperB", (data) => { return `${data.key2}`; }); cd.add("key1.nested.deeperC.wow", (data) => { return `${data.key2}`; }); cd.add("key2", (data) => "hi"); let data = { key1: { nonComputed: "hi", }, key2: "inject me", }; await cd.setupData(data); t.deepEqual(data.key1, { nonComputed: "hi", nested: { deeperA: "hi", deeperB: "hi", deeperC: { wow: "hi", }, }, }); t.is(data.key1.nested.deeperA, "hi"); t.is(data.key1.nested.deeperB, "hi"); t.is(data.key1.nested.deeperC.wow, "hi"); t.is(data.key1.nonComputed, "hi"); t.is(data.key2, "hi"); }); test("template string versus function types", async (t) => { let cd = new ComputedData(); cd.add("key1.nested.deeperA", (data) => { return `${data.key2}`; }); cd.add("key2", () => "hi"); let data = { key1: { nonComputed: "hi", }, key2: "inject me", }; await cd.setupData(data); t.deepEqual(data.key1, { nonComputed: "hi", nested: { deeperA: "hi", }, }); }); test("Basic get/set with template string", async (t) => { let cd = new ComputedData(); cd.addTemplateString("keystr", "this is a str"); cd.addTemplateString("key1", (data) => { return `this is a test ${data.key2}${data.keystr}`; }); let data = { key2: "inject me", }; await cd.setupData(data); t.is(data.key1, "this is a test inject methis is a str"); t.is(data.key2, "inject me"); t.is(data.keystr, "this is a str"); }); test("Basic get/set using array data", async (t) => { t.plan(5); let cd = new ComputedData(); cd.add("keystr", "this is a str"); cd.add("key1", (data) => { t.is(Array.isArray(data.arr), true); return `this is a test ${data.arr[0]}${data.keystr}`; }); let data = { arr: ["inject me"], collections: { first: [], second: [], }, }; await cd.setupData(data); t.is(data.key1, "this is a test inject methis is a str"); t.is(data.arr[0], "inject me"); t.is(data.keystr, "this is a str"); }); test("Computed returns deep object", async (t) => { let cd = new ComputedData(); cd.add("returnobj", (data) => { return { key1: "value1", nest: { key2: "value2", }, }; }); let data = { returnobj: { key1: "bad1", nest: { key2: "bad2", }, }, }; await cd.setupData(data); t.is(data.returnobj.key1, "value1"); t.is(data.returnobj.nest.key2, "value2"); }); test("Boolean computed value Issue #1114", async (t) => { let cd = new ComputedData(); cd.add("bool1", true); let data = { key2: "inject me", }; await cd.setupData(data); t.is(data.bool1, true); t.is(data.key2, "inject me"); }); test("Expect even missing collections to be arrays in data callback #1114", async (t) => { t.plan(2); let cd = new ComputedData(); cd.add("key1", (data) => { t.is(Array.isArray(data.collections.first), true); t.is(Array.isArray(data.collections.second), true); return ``; }); let data = { collections: {}, }; await cd.resolveVarOrder(data); }); test("Expect collections to be arrays in data callback #1114", async (t) => { t.plan(2); let cd = new ComputedData(); cd.add("key1", (data) => { if (data.collections.first.length) { t.is(data.collections.first[0], 1); t.is(data.collections.second[0], 2); } return ``; }); let data = { collections: { first: [1], second: [2], }, }; await cd.setupData(data); }); test("Get var order", async (t) => { let cd = new ComputedData(); cd.add("key1", (data) => data.collections.all); cd.add("key2", (data) => data.collections.dog); cd.add("key0", (data) => ""); let data = { key2: "inject me", collections: { all: [1], dog: [2], }, }; await cd.resolveVarOrder(data); t.deepEqual(cd.queue.getOrder(), ["collections.all", "key1", "collections.dog", "key2", "key0"]); }); test("Get var order and process it in two stages", async (t) => { let cd = new ComputedData(); cd.add("page.url", (data) => data.key2); cd.add("page.outputPath", (data) => data.key2); cd.add("key0", (data) => "hi"); cd.add("key1", (data) => data.collections.dog[0]); cd.add("collections.processed", (data) => "hi"); let data = { key2: "/my-path/", collections: { dog: [2], }, }; // set page.url, page.outputPath, key2, collections.dog[0] await cd.setupData(data, function (entry) { // TODO see note in Template.js about changing the two pass computed data return !this.isUsesStartsWith(entry, "collections."); }); t.deepEqual(data, { collections: { dog: [2], processed: "", }, key0: "hi", key1: "", key2: "/my-path/", page: { url: "/my-path/", outputPath: "/my-path/", }, }); // set collections.processed await cd.setupData(data); t.deepEqual(data, { collections: { dog: [2], processed: "hi", }, key0: "hi", key1: 2, key2: "/my-path/", page: { url: "/my-path/", outputPath: "/my-path/", }, }); // t.deepEqual(cd.queue.getOrder(), ["collections.all", "key1", "collections.dog", "key2", "key0"]); }); test("Use JavaScript functions (filters) in computed data functions", async (t) => { let eleventyCfg = new TemplateConfig(); await eleventyCfg.init(); let cfg = eleventyCfg.getConfig(); cfg.javascriptFunctions.alwaysBlue = function (str) { return str + " is blue"; }; let cd = new ComputedData(cfg); cd.add("key1", function (data) { return this.alwaysBlue("this is a test"); }); let data = {}; await cd.setupData(data); t.is(data.key1, "this is a test is blue"); }); ================================================ FILE: test/ConsoleLoggerTest.js ================================================ import test from "ava"; import ConsoleLogger from "../src/Util/ConsoleLogger.js"; test("Disable chalk", (t) => { let cl = new ConsoleLogger(); cl.isChalkEnabled = false; t.is(cl.isChalkEnabled, false); }); test("Re-enable chalk", (t) => { let cl = new ConsoleLogger(); cl.isChalkEnabled = false; cl.isChalkEnabled = true; t.is(cl.isChalkEnabled, true); }); test("Message styles", (t) => { let cl = new ConsoleLogger(); let logged; cl.message = (msg, type, color, forceToConsole) => (logged = { msg, type, color, forceToConsole }); cl.log("test"); t.deepEqual(logged, { msg: "test", type: undefined, color: undefined, forceToConsole: undefined, }); cl.forceLog("test"); t.deepEqual(logged, { msg: "test", type: undefined, color: undefined, forceToConsole: true, }); cl.info("test"); t.deepEqual(logged, { msg: "test", type: "log", color: "blue", forceToConsole: undefined, }); cl.warn("test"); t.deepEqual(logged, { msg: "test", type: "warn", color: "yellow", forceToConsole: undefined, }); cl.error("test"); t.deepEqual(logged, { msg: "test", type: "error", color: "red", forceToConsole: undefined, }); }); ================================================ FILE: test/DependencyGraphTest.js ================================================ import test from "ava"; import { DepGraph as DependencyGraph } from "dependency-graph"; test("Dependency graph nodes don’t require dependencies", async (t) => { let graph = new DependencyGraph(); graph.addNode("all"); graph.addNode("template-a"); graph.addNode("template-b"); graph.addNode("template-c"); let order = graph.overallOrder(); t.true(order.includes("all")); t.true(order.includes("template-a")); t.true(order.includes("template-b")); t.true(order.includes("template-c")); // in order of addNode t.deepEqual(graph.overallOrder(), ["all", "template-a", "template-b", "template-c"]); }); test("Dependency graph relationships", async (t) => { let graph = new DependencyGraph(); graph.addNode("all"); graph.addNode("template-a"); graph.addNode("template-b"); graph.addNode("template-c"); graph.addNode("userCollection"); graph.addDependency("all", "template-a"); graph.addDependency("all", "template-b"); graph.addDependency("all", "template-c"); graph.addDependency("userCollection", "all"); t.deepEqual(graph.overallOrder(), [ "template-a", "template-b", "template-c", "all", "userCollection", ]); }); test("Do dependencies (edges) get removed when nodes are deleted? (yes)", async (t) => { let graph = new DependencyGraph(); graph.addNode("template-a"); graph.addNode("template-b"); graph.addDependency("template-a", "template-b"); t.deepEqual(graph.overallOrder(), ["template-b", "template-a"]); t.deepEqual(graph.dependenciesOf("template-a"), ["template-b"]); graph.removeNode("template-b"); t.deepEqual(graph.dependenciesOf("template-a"), []); graph.addNode("template-b"); t.deepEqual(graph.dependenciesOf("template-a"), []); t.deepEqual(graph.overallOrder(), ["template-a", "template-b"]); }); ================================================ FILE: test/DirContainsTest.js ================================================ import test from "ava"; import DirContains from "../src/Util/DirContains.js"; test("Compare to current dir", (t) => { t.true(DirContains(".", ".")); t.false(DirContains(".", "..")); t.true(DirContains(".", "test")); t.true(DirContains(".", "./test")); t.false(DirContains(".", "../test")); }); test("Compare to parent dir", (t) => { t.true(DirContains("..", ".")); t.true(DirContains("..", "..")); t.false(DirContains("..", "../..")); t.true(DirContains("..", "test")); t.true(DirContains("..", "./test")); t.true(DirContains("..", "../test")); }); test("Compare to subfolder", (t) => { t.false(DirContains("test", ".")); t.false(DirContains("test", "..")); t.false(DirContains("test", "../..")); t.true(DirContains("test", "test")); t.true(DirContains("test", "./test")); t.false(DirContains("test", "../test")); }); test("Compare to subfolder dot slash", (t) => { t.false(DirContains("./test", ".")); t.false(DirContains("./test", "..")); t.false(DirContains("./test", "../..")); t.true(DirContains("./test", "test")); t.true(DirContains("./test", "./test")); t.false(DirContains("./test", "../test")); }); test("Compare to sibling folder", (t) => { t.false(DirContains("../test", ".")); t.false(DirContains("../test", "..")); t.false(DirContains("../test", "../..")); t.false(DirContains("../test", "test")); t.false(DirContains("../test", "./test")); t.true(DirContains("../test", "../test")); t.true(DirContains("../test", "../test/sub1")); }); ================================================ FILE: test/EleventyAddGlobalDataTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("Eleventy addGlobalData should run once", async (t) => { let count = 0; let elev = new Eleventy("./test/stubs-addglobaldata/", "./test/stubs-addglobaldata/_site", { config: function (eleventyConfig) { eleventyConfig.addGlobalData("count", () => { count++; return count; }); }, }); let results = await elev.toJSON(); t.is(count, 1); }); test("Eleventy addGlobalData shouldn’t run if no input templates match!", async (t) => { let count = 0; let elev = new Eleventy( "./test/stubs-addglobaldata-noop/", "./test/stubs-addglobaldata-noop/_site", { config: function (eleventyConfig) { eleventyConfig.addGlobalData("count", () => { count++; return count; }); }, } ); let results = await elev.toJSON(); t.is(count, 0); }); test("Eleventy addGlobalData can feed layouts to populate data cascade with layout data, issue #1245", async (t) => { let elev = new Eleventy("./test/stubs-2145/", "./test/stubs-2145/_site", { config: function (eleventyConfig) { eleventyConfig.addGlobalData("layout", () => "layout.njk"); eleventyConfig.dataFilterSelectors.add("LayoutData"); }, }); let [result] = await elev.toJSON(); t.deepEqual(result.data, { LayoutData: 123 }); t.is(result.content.trim(), "FromLayoutlayout.njk"); }); test("Eleventy addGlobalData merge data #3389", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addGlobalData("eleventyComputed", { testing(data) { return `testing:${data.page.url}`; } }); eleventyConfig.addGlobalData("eleventyComputed", { other(data) { return `other:${data.page.url}`; } }); eleventyConfig.addTemplate("computed.njk", "{{ testing }}|{{ other }}", {}) }, }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, "testing:/computed/|other:/computed/"); }); test("Eleventy addGlobalData merge data #3389 lodash set", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addGlobalData("eleventyComputed.testing", () => { return (data) => { return `testing:${data.page.url}`; } }); eleventyConfig.addGlobalData("eleventyComputed.other", () => { return (data) => { return `other:${data.page.url}`; } }); eleventyConfig.addTemplate("computed.njk", "{{ testing }}|{{ other }}", {}) }, }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, "testing:/computed/|other:/computed/"); }); test.skip("Eleventy addGlobalData merge data #3389 no nested function", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addGlobalData("eleventyComputed.testing", (data) => { return `testing:${data.page.url}`; }); eleventyConfig.addGlobalData("eleventyComputed.other", (data) => { return `other:${data.page.url}`; }); eleventyConfig.addTemplate("computed.njk", "{{ testing }}|{{ other }}", {}) }, }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, "testing:/computed/|other:/computed/"); }); ================================================ FILE: test/EleventyErrorHandlerTest.js ================================================ import test from "ava"; import { EleventyErrorHandler } from "../src/Errors/EleventyErrorHandler.js"; test("Log a warning, warning", (t) => { let errorHandler = new EleventyErrorHandler(); let output = []; errorHandler.logger = { log: function (str) { output.push(str); }, warn: function (str) { output.push(str); }, error: function (str) { output.push(str); }, message: function (str) { output.push(str); }, }; errorHandler.warn(new Error("Test warning"), "Hello"); let expected = "Hello:"; t.is(output.join("\n").slice(0, expected.length), expected); }); test("Log a warning, error", (t) => { let errorHandler = new EleventyErrorHandler(); let output = []; errorHandler.logger = { log: function (str) { output.push(str); }, warn: function (str) { output.push(str); }, error: function (str) { output.push(str); }, message: function (str) { output.push(str); }, }; errorHandler.error(new Error("Test error"), "It’s me"); let expected = `It’s me: Test error Original error stack trace: Error: Test error`; t.is(output.join("\n").slice(0, expected.length), expected); }); ================================================ FILE: test/EleventyErrorUtilTest.js ================================================ import test from "ava"; import EleventyErrorUtil from "../src/Errors/EleventyErrorUtil.js"; const SAMPLE_ERROR = new Error("Nothing to see here"); const { cleanMessage, hasEmbeddedError, convertErrorToString, deconvertErrorToObject } = EleventyErrorUtil; test("hasEmbeddedError()", (t) => { t.false(hasEmbeddedError("")); t.true(hasEmbeddedError(convertErrorToString(SAMPLE_ERROR))); }); test("cleanMessage()", (t) => { t.is(cleanMessage(null), ""); t.is(cleanMessage(undefined), ""); t.is(cleanMessage(false), ""); t.is(cleanMessage(""), ""); const text = "I am the very model of a sample text input"; t.is(cleanMessage(text), text); t.is(cleanMessage(text + convertErrorToString(SAMPLE_ERROR)), text); }); test("deconvertErrorToObject() should throw on invalid inputs", (t) => { t.throws(() => deconvertErrorToObject(undefined), { message: "Could not convert error object from: undefined", }); t.throws(() => deconvertErrorToObject(""), { message: "Could not convert error object from: ", }); t.throws(() => deconvertErrorToObject("Not an error"), { message: "Could not convert error object from: Not an error", }); }); test("deconvertErrorToObject() should return its argument if it does not contain another error", (t) => { t.is(deconvertErrorToObject(SAMPLE_ERROR), SAMPLE_ERROR); }); test("deconvertErrorToObject() should get message and stack from convertErrorToString()", (t) => { const nestingError = new Error( "This error contains a sample error: " + convertErrorToString(SAMPLE_ERROR) ); const result = deconvertErrorToObject(nestingError); t.is(result.name, nestingError.name); t.is(result.message, SAMPLE_ERROR.message); t.is(result.stack, SAMPLE_ERROR.stack); }); ================================================ FILE: test/EleventyExtensionMapTest.js ================================================ import test from "ava"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import TemplateConfig from "../src/TemplateConfig.js"; async function getExtensionMap(formats, config = new TemplateConfig()) { if (config) { await config.init(); } let map = new EleventyExtensionMap(config); map.setFormats(formats); map.engineManager = new TemplateEngineManager(config); return map; } test("Empty formats", async (t) => { let map = await getExtensionMap([]); t.deepEqual(map.getGlobs("."), []); }); test("Single format", async (t) => { let map = await getExtensionMap(["liquid"]); t.deepEqual(map.getGlobs("."), ["./**/*.liquid"]); t.deepEqual(map.getGlobs("src"), ["./src/**/*.liquid"]); }); test("Multiple formats", async (t) => { let map = await getExtensionMap(["njk", "liquid"]); t.deepEqual(map.getGlobs("."), ["./**/*.{njk,liquid}"]); t.deepEqual(map.getGlobs("src"), ["./src/**/*.{njk,liquid}"]); }); test("Invalid keys are filtered (using passthrough copy)", async (t) => { let map = await getExtensionMap(["lksdjfjlsk"]); t.deepEqual(map.getGlobs("."), ["./**/*.lksdjfjlsk"]); }); test("Keys are mapped to lower case", async (t) => { let map = await getExtensionMap(["LIQUID", "PUG", "NJK"]); t.deepEqual(map.getGlobs("."), ["./**/*.{liquid,pug,njk}"]); }); test("Pruned globs", async (t) => { let map = await getExtensionMap(["liquid", "njk", "png"]); t.deepEqual(map.getPassthroughCopyGlobs("."), ["./**/*.png"]); }); test("Empty path for fileList", async (t) => { let map = await getExtensionMap(["njk", "liquid"]); t.deepEqual(map.getFileList(), []); }); test("fileList", async (t) => { let map = await getExtensionMap(["njk", "liquid"]); t.deepEqual(map.getFileList("filename"), ["filename.njk", "filename.liquid"]); }); test("fileList with dir", async (t) => { let map = await getExtensionMap(["njk", "liquid"]); t.deepEqual(map.getFileList("filename", "_includes"), [ "_includes/filename.njk", "_includes/filename.liquid", ]); }); test("fileList with dir in path", async (t) => { let map = await getExtensionMap(["njk", "liquid"]); t.deepEqual(map.getFileList("layouts/filename"), [ "layouts/filename.njk", "layouts/filename.liquid", ]); }); test("fileList with dir in path and dir", async (t) => { let map = await getExtensionMap(["njk", "liquid", "pug"]); t.deepEqual(map.getFileList("layouts/filename", "_includes"), [ "_includes/layouts/filename.njk", "_includes/layouts/filename.liquid", ]); }); test("removeTemplateExtension", async (t) => { let map = await getExtensionMap(["njk", "11ty.js"]); t.is(map.removeTemplateExtension("component.njk"), "component"); t.is(map.removeTemplateExtension("component.11ty.js"), "component"); t.is(map.removeTemplateExtension(""), ""); t.is(map.removeTemplateExtension("component"), "component"); t.is(map.removeTemplateExtension("component.js"), "component.js"); }); test("hasEngine", async (t) => { let map = await getExtensionMap(["liquid", "njk", "11ty.js"]); t.true(map.hasEngine("default.liquid")); t.is(map.getKey("default.liquid"), "liquid"); t.falsy(map.getKey()); t.is(map.getKey("LiQuid"), "liquid"); t.true(map.hasEngine("LiqUiD")); t.true(map.hasEngine("liquid")); t.falsy(map.getKey("sldkjfkldsj")); t.false(map.hasEngine("sldkjfkldsj")); t.is(map.getKey("11ty.js"), "11ty.js"); t.true(map.hasEngine("11ty.js")); t.falsy(map.getKey("md")); t.false(map.hasEngine("md")); }); test("hasEngine no formats passed in", async (t) => { let map = await getExtensionMap([]); t.false(map.hasEngine("default.liquid")); t.falsy(map.getKey("default.liquid")); t.falsy(map.getKey()); t.falsy(map.getKey("LiQuid")); t.false(map.hasEngine("LiqUiD")); t.false(map.hasEngine("liquid")); t.falsy(map.getKey("sldkjfkldsj")); t.false(map.hasEngine("sldkjfkldsj")); t.falsy(map.getKey("11ty.js")); t.false(map.hasEngine("11ty.js")); t.falsy(map.getKey("md")); t.false(map.hasEngine("md")); }); test("getKey", async (t) => { let map = await getExtensionMap(["njk", "11ty.js", "md"]); t.is(map.getKey("component.njk"), "njk"); t.is(map.getKey("component.11ty.js"), "11ty.js"); t.is(map.getKey("11ty.js"), "11ty.js"); t.is(map.getKey(".11ty.js"), "11ty.js"); t.is(map.getKey("sample.md"), "md"); t.is(map.getKey(""), undefined); t.is(map.getKey("js"), undefined); t.is(map.getKey("component"), undefined); t.is(map.getKey("component.js"), undefined); }); test("isFullTemplateFilePath (not a passthrough copy extension)", async (t) => { let map = await getExtensionMap(["liquid", "njk", "11ty.js", "js", "css"]); t.true(map.isFullTemplateFilePath("template.liquid")); t.true(map.isFullTemplateFilePath("template.njk")); t.true(map.isFullTemplateFilePath("template.11ty.js")); t.false(map.isFullTemplateFilePath("template.ejs")); t.false(map.isFullTemplateFilePath("template.pug")); t.false(map.isFullTemplateFilePath("passthrough.js")); t.false(map.isFullTemplateFilePath("passthrough.css")); }); test("getValidExtensionsForPath", async (t) => { let cfg = new TemplateConfig(); cfg.userConfig.extensionMap.add({ key: "js", extension: "js", }); await cfg.init(); let map = await getExtensionMap(["liquid", "njk", "11ty.js", "js"], cfg); t.deepEqual(map.getValidExtensionsForPath("template.liquid"), ["liquid"]); t.deepEqual(map.getValidExtensionsForPath("template.11ty.js"), ["11ty.js", "js"]); t.deepEqual(map.getValidExtensionsForPath("template.pug"), []); t.deepEqual(map.getValidExtensionsForPath("template.liquid.js"), ["js"]); t.deepEqual(map.getValidExtensionsForPath("njk.liquid.11ty.js"), ["11ty.js", "js"]); }); test("shouldSpiderJavaScriptDependencies", async (t) => { let cfg = new TemplateConfig(); cfg.userConfig.extensionMap.add({ key: "js", extension: "js", }); await cfg.init(); let map = await getExtensionMap(["liquid", "njk", "11ty.js", "js"], cfg); t.deepEqual(await map.shouldSpiderJavaScriptDependencies("template.liquid"), false); t.deepEqual(await map.shouldSpiderJavaScriptDependencies("template.njk"), false); t.deepEqual(await map.shouldSpiderJavaScriptDependencies("template.css"), false); t.deepEqual(await map.shouldSpiderJavaScriptDependencies("template.11ty.js"), true); t.deepEqual(await map.shouldSpiderJavaScriptDependencies("template.js"), false); }); ================================================ FILE: test/EleventyFilesGitIgnoreEleventyIgnoreTest.js ================================================ import test from "ava"; import { TemplatePath } from "@11ty/eleventy-utils"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback, getEleventyFilesInstance } from "./_testHelpers.js"; /* .eleventyignore and .gitignore combos */ test("Get ignores (no .eleventyignore no .gitignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore1", output: "test/stubs/ignore1/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalroot"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalroot/test.md", "./test/stubs/ignore1/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalroot/.git/**", ]); }); test("Get ignores (no .eleventyignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore2", output: "test/stubs/ignore2/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalrootgitignore"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalrootgitignore/thisshouldnotexist12345", "./test/stubs/ignorelocalrootgitignore/test.md", "./test/stubs/ignore2/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalrootgitignore/.git/**", ]); }); test("Get ignores (no .eleventyignore, using setUseGitIgnore(false))", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/ignore2", output: "test/stubs/ignore2/_site", }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalroot"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalroot/test.md", "./test/stubs/ignore2/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalroot/.git/**", ]); }); test("Get ignores (no .gitignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore3", output: "test/stubs/ignore3/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalroot"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalroot/test.md", "./test/stubs/ignore3/ignoredFolder/**", "./test/stubs/ignore3/ignoredFolder/ignored.md", "./test/stubs/ignore3/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalroot/.git/**", ]); }); test("Get ignores (project .eleventyignore and root .gitignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore4", output: "test/stubs/ignore4/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalrootgitignore"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalrootgitignore/thisshouldnotexist12345", "./test/stubs/ignorelocalrootgitignore/test.md", "./test/stubs/ignore4/ignoredFolder/**", "./test/stubs/ignore4/ignoredFolder/ignored.md", "./test/stubs/ignore4/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalrootgitignore/.git/**", ]); }); test("Get ignores (project .eleventyignore and root .gitignore, using setUseGitIgnore(false))", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/ignore4", output: "test/stubs/ignore4/_site", }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalrootgitignore"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalrootgitignore/test.md", "./test/stubs/ignore4/ignoredFolder/**", "./test/stubs/ignore4/ignoredFolder/ignored.md", "./test/stubs/ignore4/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalrootgitignore/.git/**", ]); }); test("Get ignores (no .eleventyignore .gitignore exists but empty)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore5", output: "test/stubs/ignore5/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalroot"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalroot/test.md", "./test/stubs/ignore5/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalroot/.git/**", ]); }); test("Get ignores (both .eleventyignore and .gitignore exists, but .gitignore is empty)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore6", output: "test/stubs/ignore6/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignorelocalroot"); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignorelocalroot/test.md", "./test/stubs/ignore6/ignoredFolder/**", "./test/stubs/ignore6/ignoredFolder/ignored.md", "./test/stubs/ignore6/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignorelocalroot/.git/**", ]); }); test("Bad expected output, this indicates a bug upstream in a dependency (update, was fixed in fast-glob@3.3.3). Input to 'src' and empty includes dir (issue #403, full paths in eleventyignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-403", output: "_site", includes: "", data: false, }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid"], eleventyConfig); evf._setEleventyIgnoreContent(TemplatePath.absolutePath("test/stubs-403/_includes") + "/**"); evf.init(); // duplicate init t.deepEqual(await evf.getFiles(), [ "./test/stubs-403/template.liquid", // UPDATE: this was fixed in fast-glob@3.3.3 // This should be excluded from this list but is not because the ignore content used an absolutePath above. // "./test/stubs-403/_includes/include.liquid", ]); }); test("Workaround for Bad expected output, this indicates a bug upstream in a dependency. Input to 'src' and empty includes dir (issue #403, full paths in eleventyignore)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-403", output: "_site", includes: "", data: false, }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid"], eleventyConfig); evf._setEleventyIgnoreContent("./test/stubs-403/_includes/**"); evf.init(); // duplicate init t.deepEqual(await evf.getFiles(), ["./test/stubs-403/template.liquid"]); }); test("Issue #403: all .eleventyignores should be relative paths not absolute paths", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-403", output: "_site", includes: "", data: false, }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid"], eleventyConfig); let globs = await evf.getFileGlobs(); t.is( globs.filter((glob) => { return glob.indexOf(TemplatePath.absolutePath()) > -1; }).length, 0 ); }); test("Same input and output directories, issues #186 and #1129", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "", }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); t.deepEqual( evf.getIgnores().filter((entry) => entry.indexOf("_site") > -1), [] ); }); test("Single input file is in the output directory, issues #186", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "", includes: "", }, function(eleventyConfig) { eleventyConfig.setUseGitIgnore(false); }); let { eleventyFiles: evf } = getEleventyFilesInstance(["njk"], eleventyConfig); t.deepEqual( evf.getIgnores().filter((entry) => entry.indexOf("_site") > -1), [] ); }); test("De-duplicated ignores", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/ignore-dedupe", output: "test/stubs/ignore-dedupe/_site" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf._setLocalPathRoot("./test/stubs/ignore-dedupe"); t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/ignore-dedupe/_includes/**", "./test/stubs/ignore-dedupe/_data/**", ]); t.deepEqual(evf.getIgnores(), [ "./test/stubs/ignore-dedupe/ignoredFolder", "./test/stubs/ignore-dedupe/_site/**", ]); t.deepEqual(evf.getIgnoreGlobs().slice(-2), [ "**/node_modules/**", "./test/stubs/ignore-dedupe/.git/**", ]); }); ================================================ FILE: test/EleventyFilesTest.js ================================================ import test from "ava"; import { glob } from "tinyglobby"; import EleventyFiles from "../src/EleventyFiles.js"; import TemplateConfig from "../src/TemplateConfig.js"; import TemplatePassthroughManager from "../src/TemplatePassthroughManager.js"; import ProjectDirectories from "../src/Util/ProjectDirectories.js"; import { isTypeScriptSupported } from "../src/Util/FeatureTests.cjs"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback, getEleventyFilesInstance } from "./_testHelpers.js"; test("Dirs paths", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "src", includes: "includes", data: "data", output: "dist", } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); t.deepEqual(evf.inputDir, "./src/"); t.deepEqual(evf.includesDir, "./src/includes/"); t.deepEqual(evf.getDataDir(), "./src/data/"); t.deepEqual(evf.outputDir, "./dist/"); }); test("Dirs paths (relative)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "src", includes: "../includes", data: "../data", output: "dist", }, }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); t.deepEqual(evf.inputDir, "./src/"); t.deepEqual(evf.includesDir, "./includes/"); t.deepEqual(evf.getDataDir(), "./data/"); t.deepEqual(evf.outputDir, "./dist/"); }); test("getFiles", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTest", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md"], eleventyConfig); t.deepEqual(await evf.getFiles(), ["./test/stubs/writeTest/test.md"]); }); test("getFiles (without 11ty.js)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTestJS" } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md"], eleventyConfig); t.deepEqual(await evf.getFiles(), []); }); test("getFiles (with 11ty.js)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTestJS", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md", "11ty.js"], eleventyConfig); t.deepEqual(await evf.getFiles(), ["./test/stubs/writeTestJS/test.11ty.cjs"]); }); test("getFiles (with js, treated as passthrough copy)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTestJS-passthrough", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md", "js", "11ty.js"], eleventyConfig); const files = await evf.getFiles(); t.deepEqual( files.sort(), [ "./test/stubs/writeTestJS-passthrough/sample.js", "./test/stubs/writeTestJS-passthrough/test.11ty.js", ].sort() ); t.false(evf.extensionMap.hasEngine("./test/stubs/writeTestJS-passthrough/sample.js")); t.true(evf.extensionMap.hasEngine("./test/stubs/writeTestJS-passthrough/test.11ty.js")); }); test("getFiles (with case insensitivity)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTestJS-casesensitive", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["11ty.js", "JS"], eleventyConfig); t.deepEqual( (await evf.getFiles()).sort(), [ "./test/stubs/writeTestJS-casesensitive/sample.Js", "./test/stubs/writeTestJS-casesensitive/test.11Ty.js", ].sort() ); t.false(evf.extensionMap.hasEngine("./test/stubs/writeTestJS-casesensitive/sample.Js")); t.true(evf.extensionMap.hasEngine("./test/stubs/writeTestJS-casesensitive/test.11Ty.js")); }); test("Mutually exclusive Input and Output dirs", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/writeTest", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md"], eleventyConfig); let files = await glob(evf.getFileGlobs()); t.deepEqual(evf.getRawFiles(), ["./test/stubs/writeTest/**/*.{liquid,md}"]); t.true(files.length > 0); t.is(files[0], "test/stubs/writeTest/test.md"); }); test("Single File Input (deep path)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/index.html", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md"], eleventyConfig); let files = await glob(evf.getFileGlobs()); t.is(evf.getRawFiles().length, 1); t.is(files.length, 1); t.is(files[0], "test/stubs/index.html"); }); test("Single File Input (shallow path)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "README.md", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["md"], eleventyConfig); let globs = evf.getFileGlobs(); //.filter((path) => path !== "./README.md"); let files = await glob(globs, { ignore: evf.getIgnoreGlobs(), }); t.is(evf.getRawFiles().length, 1); t.is(files.length, 1); t.is(files[0], "README.md"); }); test("Glob Input", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/glob-pages/!(contact.md)", } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["md"], eleventyConfig); let globs = evf.getFileGlobs(); let files = await glob(globs); t.is(files.length, 2); t.is(files[0], "test/stubs/glob-pages/about.md"); t.is(files[1], "test/stubs/glob-pages/home.md"); }); test(".eleventyignore parsing", (t) => { let ignores = EleventyFiles.getFileIgnores("./test/stubs/.eleventyignore"); t.is(ignores.length, 2); t.is(ignores[0], "./test/stubs/ignoredFolder/**"); t.is(ignores[1], "./test/stubs/ignoredFolder/ignored.md"); }); test("Parse multiple .eleventyignores", (t) => { let ignores = EleventyFiles.getFileIgnores([ "./test/stubs/multiple-ignores/.eleventyignore", "./test/stubs/multiple-ignores/subfolder/.eleventyignore", ]); t.is(ignores.length, 4); // Note these folders must exist! t.is(ignores[0], "./test/stubs/multiple-ignores/ignoredFolder/**"); t.is(ignores[1], "./test/stubs/multiple-ignores/ignoredFolder/ignored.md"); t.is(ignores[2], "./test/stubs/multiple-ignores/subfolder/ignoredFolder2/**"); t.is(ignores[3], "./test/stubs/multiple-ignores/subfolder/ignoredFolder2/ignored2.md"); }); test("Passed file name does not exist", (t) => { let ignores = EleventyFiles.getFileIgnores(".thisfiledoesnotexist"); t.deepEqual(ignores, []); }); test(".eleventyignore files", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["liquid", "md"], eleventyConfig); let ignoredFiles = await glob("test/stubs/ignoredFolder/*.md"); t.is(ignoredFiles.length, 1); let files = await glob(evf.getFileGlobs(), { ignore: evf.getIgnoreGlobs(), }); t.true(files.length > 0); t.is( files.filter((file) => { return file.indexOf("./test/stubs/ignoredFolder") > -1; }).length, 0 ); }); test("getTemplateData caching", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf.init(); let templateDataFirstCall = evf.templateData; let templateDataSecondCall = evf.templateData; t.is(templateDataFirstCall, templateDataSecondCall); }); test("getDataDir", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "." } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf.init(); t.is(evf.getDataDir(), "./_data/"); }); test("getDataDir subdir", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf.init(); t.is(evf.getDataDir(), "./test/stubs/_data/"); }); test("Include and Data Dirs", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf.init(); t.deepEqual(evf.getIncludesAndDataDirs(), [ "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); }); test("Input to 'src' and empty includes dir (issue #403)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "src" } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["md", "liquid", "html"], eleventyConfig); evf._setEleventyIgnoreContent("!./src/_includes/**"); evf._setConfig({ useGitIgnore: false, dir: { input: ".", output: "_site", includes: "", data: "_data", }, }); evf.init(); // duplicate init t.deepEqual(evf.getFileGlobs(), [ "./src/**/*.{md,liquid,html}", // "!./src/_includes/**", // "!./src/_site/**", // "!./src/_data/**", ]); }); test("Glob Watcher Files", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["njk"], eleventyConfig); t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/**/*.njk", "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); }); test("Glob Watcher Files with File Extension Passthroughs", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let { eleventyFiles: evf } = getEleventyFilesInstance(["njk", "png"], eleventyConfig); t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/**/*.njk", "./test/stubs/**/*.png", "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); }); test("Glob Watcher Files with File Extension Passthroughs with Dev Server (for free passthrough copy #2456)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); eleventyConfig.userConfig.setServerPassthroughCopyBehavior("passthrough"); eleventyConfig.config.serverPassthroughCopyBehavior = "passthrough"; let { eleventyFiles: evf } = getEleventyFilesInstance(["njk", "png"], eleventyConfig); evf.setRunMode("serve"); evf.init(); // duplicate init t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/**/*.njk", "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); t.deepEqual(evf.getGlobWatcherFilesForPassthroughCopy(), ["./test/stubs/**/*.png"]); }); test("Glob Watcher Files with Config Passthroughs (one template format)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "test/stubs/_site" }, function(cfg) { cfg.passthroughCopies = { "test/stubs/img/": { outputPath: true }, }; }); let { eleventyFiles: evf } = getEleventyFilesInstance(["njk"], eleventyConfig); t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/**/*.njk", "./test/stubs/img/**", "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); }); test("Glob Watcher Files with Config Passthroughs (one template format) with Dev Server (for free passthrough copy #2456)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setServerPassthroughCopyBehavior("passthrough"); cfg.passthroughCopies = { "test/stubs/img/": { outputPath: true }, }; }); let { eleventyFiles: evf } = getEleventyFilesInstance(["njk"], eleventyConfig); evf.setRunMode("serve"); evf.init(); // duplicate init let mgr = new TemplatePassthroughManager(eleventyConfig); evf.setPassthroughManager(mgr); t.deepEqual(evf.getGlobWatcherFiles(), [ "./test/stubs/**/*.njk", "./test/stubs/_includes/**", "./test/stubs/_data/**", ]); t.deepEqual(evf.getGlobWatcherFilesForPassthroughCopy(), ["./test/stubs/img/**"]); }); test("Glob Watcher Files with Config Passthroughs (no template formats)", async (t) => { let templateConfig = new TemplateConfig(); let projectDirs = new ProjectDirectories(); projectDirs.setViaConfigObject({ input: "test/stubs" }); let eleventyConfig = await getTemplateConfigInstance(templateConfig, projectDirs); let { eleventyFiles: evf } = getEleventyFilesInstance([], eleventyConfig); evf.init(); t.deepEqual(await evf.getGlobWatcherTemplateDataFiles(), [ `./test/stubs/**/*.{json,11tydata.mjs,11tydata.cjs,11tydata.js${isTypeScriptSupported() ? ",11tydata.mts,11tydata.cts,11tydata.ts" : ""}}`, ]); }); test("Test that negations are ignored (for now) PR#709, will change when #693 is implemented", async (t) => { t.deepEqual( EleventyFiles.normalizeIgnoreContent( "./", `hello !testing` ), ["./hello"] ); }); ================================================ FILE: test/EleventyImgTransformTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; test("Default image transform with a single image", async (t) => { let elev = new Eleventy("./test/stubs-img-transform/single.md", "./test/stubs-img-transform/_site", { config: eleventyConfig => { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: "html", dryRun: true, formats: ["auto"], defaultAttributes: { loading: "lazy", decoding: "async" } }); } }); let [result] = await elev.toJSON(); t.deepEqual(normalizeNewLines(result.content), `it’s a possum`); }); test("Default image transform with multiple images", async (t) => { let elev = new Eleventy("./test/stubs-img-transform/multiple.md", "./test/stubs-img-transform/_site", { config: eleventyConfig => { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: "html", dryRun: true, formats: ["auto"], defaultAttributes: { loading: "lazy", decoding: "async" } }); } }); let [result] = await elev.toJSON(); t.deepEqual(normalizeNewLines(result.content), `it’s a possum it’s a possum`); }); test("Default image transform with an ignored image", async (t) => { let elev = new Eleventy("./test/stubs-img-transform/ignored.md", "./test/stubs-img-transform/_site", { config: eleventyConfig => { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: "html", dryRun: true, formats: ["auto"], defaultAttributes: { loading: "lazy", decoding: "async" } }); } }); let [result] = await elev.toJSON(); t.deepEqual(normalizeNewLines(result.content), `it’s a possum`); }); test("Missing alt", async (t) => { let elev = new Eleventy("./test/stubs-img-transform/missing-alt.md", "./test/stubs-img-transform/_site", { config: eleventyConfig => { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: "html", dryRun: true, formats: ["auto"], }); } }); elev.setIsVerbose(false); elev.disableLogger(); await t.throwsAsync(async () => { await elev.toJSON(); }); }); ================================================ FILE: test/EleventyMarkdownTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("Markdown in markdown #3954", async (t) => { let elev = new Eleventy({ input: "./test/stubs-virtual/", config: eleventyConfig => { eleventyConfig.addTemplate("_includes/layout.md", `{{ content }}`); eleventyConfig.addTemplate("index.md", `--- layout: layout.md --- # Heading \`\`\` # This is code # This is another code \`\`\``); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `

Heading

# This is code

# This is another code
`); }); test("Preprocess Markdown with markdown #3925", async (t) => { let elev = new Eleventy({ input: "./test/stubs-virtual/", config: eleventyConfig => { eleventyConfig.setMarkdownTemplateEngine("md"); eleventyConfig.addTemplate("index.md", `# Heading \`\`\` # This is code # This is another code \`\`\``); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `

Heading

# This is code

# This is another code
`); }); ================================================ FILE: test/EleventyNunjucksTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("Paired shortcodes in macros #2261 #1749", async (t) => { let elev = new Eleventy({ input: "./test/stubs-2261/", configPath: "./test/stubs-2261/eleventy.config.js", }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `
HelloHello Manuel
`); }); ================================================ FILE: test/EleventyServeTest.js ================================================ import test from "ava"; import EleventyServe from "../src/EleventyServe.js"; import TemplateConfig from "../src/TemplateConfig.js"; async function getServerInstance(eleventyConfig) { let es = new EleventyServe(); if (!eleventyConfig) { eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); } es.eleventyConfig = eleventyConfig; await es.init(); delete es.options.logger; delete es.options.module; return es; } test("Constructor", async (t) => { let es = await getServerInstance(); t.is(es.options.pathPrefix, "/"); }); test("Get Options", async (t) => { let es = await getServerInstance(); es.setOutputDir("_site"); t.deepEqual(es.options, { pathPrefix: "/", port: 8080, }); }); test("Get Options (with a pathPrefix)", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init({ pathPrefix: "/web/" }); let es = await getServerInstance(eleventyConfig); es.setOutputDir("_site"); t.deepEqual(es.options, { pathPrefix: "/web/", port: 8080, }); }); test("Get Options (override in config)", async (t) => { let es = await getServerInstance(); es.setOutputDir("_site"); t.deepEqual(es.options, { pathPrefix: "/", port: 8080, }); }); test("Sanity test that default output is set correctly", async (t) => { let es = await getServerInstance(); es.setOutputDir("_site"); await es.initServerInstance(); t.is(es.server.dir, "_site"); }); // This assert should work once updating the output dir of the server works. test("Custom output dir is set correctly", async (t) => { let es = await getServerInstance(); es.setOutputDir("x"); await es.initServerInstance(); t.is(es.outputDir, "x"); t.is(es.server.dir, "x"); }); ================================================ FILE: test/EleventyTest-CustomDateParsing.js ================================================ import { createRequire } from "node:module"; import test from "ava"; import { DateTime } from "luxon"; import Eleventy from "../src/Eleventy.js"; const require = createRequire(import.meta.url); test("Custom date parsing callback (return string), Issue #867", async (t) => { t.plan(3); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function(dateValue) { t.truthy(this.page.inputPath); t.is(dateValue, undefined); return "2001-01-01T12:00:00Z"; }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12))); }); test("Custom date parsing callback (input a non-YAML date format), Issue #867", async (t) => { t.plan(2); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `--- date: 2019-08-31 23:59:56 America/New_York --- # Markdown`); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function(dateValue) { t.is(dateValue, "2019-08-31 23:59:56 America/New_York"); // returns DateTime instance from Luxon return DateTime.fromFormat(dateValue, "yyyy-MM-dd hh:mm:ss z"); }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2019,8,1,3,59,56))); }); test("Custom date parsing callback (return Date), Issue #867", async (t) => { t.plan(3); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function(dateValue) { t.truthy(this.page.inputPath); t.is(dateValue, undefined); return new Date(Date.UTC(2001,0,1,12)); }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12))); }); test("Custom date parsing callback (using date from data cascade, return string), Issue #867", async (t) => { t.plan(3); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`, { date: new Date(Date.UTC(2002, 0, 1, 12)) }); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); t.true(dateValue instanceof Date); return "2001-01-01T12:00:00Z"; }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12))); }); test("Custom date parsing callback (two, return undefined/falsy), Issue #867", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`, { date: new Date(Date.UTC(2003, 0, 1, 12)) }); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); t.deepEqual(dateValue, new Date(Date.UTC(2003,0,1,12))); // return nothing }); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); t.deepEqual(dateValue, new Date(Date.UTC(2003,0,1,12))); // return nothing }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2003,0,1,12))); }); test("Custom date parsing callback (return explicit false), Issue #867", async (t) => { t.plan(2); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`, { date: new Date(Date.UTC(2003, 0, 1, 12)) }); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); return false; }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2003,0,1,12))); }); test("Custom date parsing callbacks (two, last wins, return string), Issue #867", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); t.is(dateValue, undefined); return "2010-01-01T12:00:00Z"; }); eleventyConfig.addDateParsing(function (dateValue) { t.truthy(this.page.inputPath); t.is(dateValue, "2010-01-01T12:00:00Z"); return "2001-01-01T12:00:00Z"; }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12))); }); // https://github.com/11ty/eleventy/issues/3674 test("instanceof DateTime issue, Issue #3674", async (t) => { const { DateTime } = require("luxon"); // this test *requires* a non-virtual template to repro let elev = new Eleventy("./test/stubs/index.html", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`); eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addDateParsing(function (dateValue) { return DateTime.fromISO("2001-01-01T12:00:00Z"); }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12))); }); ================================================ FILE: test/EleventyTest-PageData.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3794: page.inputPathDir and page.dir", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("test.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, {}); } }); let [result] = await elev.toJSON(); t.is(result.content, "./test/stubs-virtual/ and /test/"); }); test("#3794: page.inputPathDir and page.dir (index file)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, {}); } }); let [result] = await elev.toJSON(); t.is(result.content, "./test/stubs-virtual/ and /"); }); test("#3794: page.inputPathDir and page.dir (paginated)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, { data: [1,2,3], pagination: { data: "data", size: 1, } }); } }); let results = await elev.toJSON(); t.is(results.length, 3); let [page1, page2, page3] = results; t.is(page1.content, "./test/stubs-virtual/ and /"); t.is(page2.content, "./test/stubs-virtual/ and /1/"); t.is(page3.content, "./test/stubs-virtual/ and /2/"); }); test("#3794: page.inputPathDir and page.dir (with file slug and index)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("yawn/index.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, { permalink: "{{ page.filePathStem }}.{{ page.outputFileExtension }}" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); let [page1] = results; t.is(page1.content, "./test/stubs-virtual/yawn/ and /yawn/"); }); test("#3794: page.inputPathDir and page.dir (with file slug and not-index)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("yawn/test.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, { permalink: "{{ page.filePathStem }}.{{ page.outputFileExtension }}" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); let [page1] = results; t.is(page1.content, "./test/stubs-virtual/yawn/ and /yawn/"); }); test("#3794: page.inputPathDir and page.dir (paginated with file slug and not-index)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("yawn/test.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, { data: [1,2,3], pagination: { data: "data", size: 1, }, permalink: "/{{ pagination.pageNumber }}{{ page.filePathStem }}.{{ page.outputFileExtension }}" }); } }); let results = await elev.toJSON(); t.is(results.length, 3); let [page1, page2, page3] = results; t.is(page1.content, "./test/stubs-virtual/yawn/ and /0/yawn/"); t.is(page2.content, "./test/stubs-virtual/yawn/ and /1/yawn/"); t.is(page3.content, "./test/stubs-virtual/yawn/ and /2/yawn/"); }); test("#3794: page.inputPathDir and page.dir (permalink: false)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `{{ page.inputPathDir }} and {{ page.dir }}`, { permalink: false }); } }); let [result] = await elev.toJSON(); t.is(result.content, "./test/stubs-virtual/ and false"); }); ================================================ FILE: test/EleventyTest-Preprocessors.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#188: Content preprocessing (dot in file extension)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", ".njk", (data, content) => { if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("index.njk", "Before"); eleventyConfig.addTemplate("draft.njk", "Before", { draft: true }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `Hello Before`); }); test("#188: Content preprocessing (no dot in file extension)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", "njk", (data, content) => { if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("index.njk", "Before"); eleventyConfig.addTemplate("draft.njk", "Before", { draft: true }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `Hello Before`); }); test("#188: Content preprocessing (array, no dot in file extension)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", ["njk"], (data, content) => { if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("index.njk", "Before"); eleventyConfig.addTemplate("draft.njk", "Before", { draft: true }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `Hello Before`); }); test("#188: Content preprocessing (array, dot in file extension)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", [".njk"], (data, content) => { if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("index.njk", "Before"); eleventyConfig.addTemplate("draft.njk", "Before", { draft: true }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `Hello Before`); }); test("#188: Content preprocessing (wildcard)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", "*", (data, content) => { if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("index.njk", "Before"); eleventyConfig.addTemplate("draft.njk", "Before", { draft: true }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `Hello Before`); }); test("addPreprocessor with 11ty.js, Issue #3433", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("testing", "11ty.js", (data, content) => { t.is( typeof content, "function" ); t.is(content(), "Hello!"); return { render: function() { return "naw"; } }; }); eleventyConfig.addTemplate("template.11ty.js", function() { return "Hello!" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/template/`); t.is(results[0].content.trim(), `naw`); }); test("addPreprocessor and addExtension, Issue #3433", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplateFormats("11ty.test"); eleventyConfig.addExtension("11ty.test", { key: "11ty.js", }); eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => { t.is( typeof content, "function" ); t.is(content(), "Hello!"); return { render: function() { return "naw"; } }; }); eleventyConfig.addTemplate("template.11ty.test", function() { return "Hello!" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/template/`); t.is(results[0].content.trim(), `naw`); }); test("addPreprocessor and addExtension with custom `compile` (defaultRenderer), Issue #3433", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplateFormats("11ty.test"); eleventyConfig.addExtension("11ty.test", { key: "11ty.js", compile: function() { return this.defaultRenderer; } }); eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => { t.is( typeof content, "function" ); t.is(content(), "Hello!"); return { render: function() { return "naw"; } }; }); eleventyConfig.addTemplate("template.11ty.test", function() { return "Hello!" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/template/`); t.is(results[0].content.trim(), `naw`); }); test("addPreprocessor and addExtension with custom `compile` (re-use render function directly), Issue #3433", async (t) => { t.plan(5); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplateFormats("11ty.test"); eleventyConfig.addExtension("11ty.test", { key: "11ty.js", compile: function(content) { return function() { return content.render(); } } }); eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => { t.is( typeof content, "function" ); t.is(content(), "Hello!"); return { render: function() { return "naw"; } }; }); eleventyConfig.addTemplate("template.11ty.test", function() { return "Hello!" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/template/`); t.is(results[0].content.trim(), `naw`); }); test("addPreprocessor and addExtension with custom `compile` (new render function), Issue #3433", async (t) => { t.plan(7); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplateFormats("11ty.test"); eleventyConfig.addExtension("11ty.test", { key: "11ty.js", compile: function(content) { // check preprocessor override t.is( typeof content.render, "function" ); t.is(content.render(), "Preprocessor override"); return function() { return "Compiled content"; } } }); eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => { // check template content directly t.is( typeof content, "function" ); t.is(content(), "Original template content"); return { render: function() { return "Preprocessor override"; } }; }); eleventyConfig.addTemplate("template.11ty.test", function() { return "Original template content" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/template/`); t.is(results[0].content.trim(), `Compiled content`); }); // #3933 test("Tags in pages excluded with preprocessing should not populate collections props", async (t) => { let preprocessorRuns = 0; let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addPreprocessor("drafts", "njk", (data, content) => { preprocessorRuns++; if(data.draft) { return false; } return `Hello ${content}`; }); eleventyConfig.addTemplate("paged.njk", "{{ tag }}", { pagination: { data: "collections", size: 1, alias: "tag", filter: ["all"], }, permalink: "/{{ tag }}/" }); eleventyConfig.addTemplate("source.njk", "Before", { tags: ["yep"] }); eleventyConfig.addTemplate("source-draft.njk", "Before", { draft: true, tags: ["nope"] }); } }); let results = await elev.toJSON(); t.is(preprocessorRuns, 3); t.is(results.length, 2); t.truthy(results.find(entry => entry.inputPath.endsWith("source.njk"))); t.falsy(results.find(entry => entry.inputPath.endsWith("source-draft.njk"))); let pages = results.filter(entry => entry.inputPath.endsWith("paged.njk")); t.is(pages.length, 1); t.is(pages[0].content, "Hello yep"); }); ================================================ FILE: test/EleventyTest-Shortcodes.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test.skip("#3400: Both a paired and unpaired shortcode.", async (t) => { let count = 0; let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function(eleventyConfig) { eleventyConfig.addShortcode("single", function() { count++; }); eleventyConfig.addPairedShortcode("single", function() { count++; }); eleventyConfig.addTemplate("test.njk", `{% single %} {% single %}{% endsingle %}`, {}); } }); let results = await elev.toJSON(); t.is(count, 2); }); ================================================ FILE: test/EleventyTest.js ================================================ import test from "ava"; import fs from "node:fs"; import path from "node:path"; import lodash from "@11ty/lodash-custom"; import { z } from "zod"; import { fromZodError } from "zod-validation-error"; import { marked } from "marked"; import nunjucks from "@11ty/nunjucks"; import * as sass from "sass"; import Eleventy, { HtmlBasePlugin } from "../src/Eleventy.js"; import TemplateContent from "../src/TemplateContent.js"; import TemplateMap from "../src/TemplateMap.js"; import TemplateConfig from "../src/TemplateConfig.js"; import { getCreatedTimestamp, getUpdatedTimestamp } from "../src/Util/Git.js"; import PathNormalizer from "../src/Util/PathNormalizer.js"; import { normalizeNewLines, localizeNewLines } from "./Util/normalizeNewLines.js"; import { isTypeScriptSupported } from "../src/Util/FeatureTests.cjs"; import { deleteDirectory } from "./_testHelpers.js"; const lodashGet = lodash.get; test("Eleventy, defaults inherit from config", async (t) => { let elev = new Eleventy(); let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); await elev.initializeConfig(); let config = eleventyConfig.getConfig(); t.truthy(elev.input); t.truthy(elev.outputDir); t.is(config.dir.input, "."); t.is(elev.input, "./"); t.is(config.dir.output, "_site"); t.is(elev.outputDir, "./_site/"); }); test("Eleventy, null output directory should default to _site", async (t) => { let elev = new Eleventy(".", null); let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); await elev.initializeConfig(); let config = eleventyConfig.getConfig(); t.is(config.dir.input, "."); t.is(elev.input, "./"); t.is(config.dir.output, "_site"); t.is(elev.outputDir, "./_site/"); }); test("Eleventy, get version", (t) => { let elev = new Eleventy(); t.truthy(elev.getVersion()); }); test("Eleventy, get help", (t) => { let elev = new Eleventy(); t.truthy(elev.getHelp()); }); test("Eleventy, set is verbose (before config init)", async (t) => { let elev = new Eleventy(); elev.setIsVerbose(true); await elev.initializeConfig(); t.true(elev.verboseMode); }); test("Eleventy, set is verbose (after config init)", async (t) => { let elev = new Eleventy(); await elev.initializeConfig(); elev.setIsVerbose(true); t.true(elev.verboseMode); }); test("Eleventy set input/output", async (t) => { let elev = new Eleventy("./test/stubs", "./test/stubs/_site"); t.is(elev.input, "./test/stubs/"); t.is(elev.outputDir, "./test/stubs/_site/"); await elev.init(); t.truthy(elev.templateData); t.truthy(elev.writer); }); test("Eleventy process.ENV", async (t) => { delete process.env.ELEVENTY_ROOT; t.falsy(process.env.ELEVENTY_ROOT); let elev = new Eleventy("./test/stubs", "./test/stubs/_site"); await elev.init(); t.truthy(process.env.ELEVENTY_ROOT); // all ELEVENTY_ env variables are also available on eleventy.env let globals = await elev.templateData.getInitialGlobalData(); t.truthy(globals.eleventy.env.root); }); test("Eleventy file watching", async (t) => { let elev = new Eleventy("./test/stubs", "./test/stubs/_site", { runMode: "watch" // required to spider deps }); elev.setFormats("njk"); elev.disableLogger(); await elev.init(); let globalData = await elev.templateData.getGlobalData(); await elev.eleventyFiles.getFiles(); await elev.startWatch(); let { targets, ignores } = await elev.getWatchedTargets(); await elev.stopWatch(); t.deepEqual(targets, [ "./package.json", "./test/stubs/**/*.njk", "./test/stubs/_includes/**", "./test/stubs/_data/**", "./.gitignore", "./.eleventyignore", "./test/stubs/.eleventyignore", `./test/stubs/**/*.{json,11tydata.mjs,11tydata.cjs,11tydata.js${isTypeScriptSupported() ? ",11tydata.mts,11tydata.cts,11tydata.ts" : ""}}`, "./test/stubs/deps/dep1.cjs", "./test/stubs/deps/dep2.cjs", ]); t.true(ignores.includes("node_modules/**")); t.true(ignores.includes("**/node_modules/**")); t.true(ignores.includes("test/stubs/_site/**")); t.true(ignores.includes("./.git/**")); t.true(ignores.includes(".cache")); }); test("Eleventy file watching (don’t watch deps of passthrough copy .js files)", async (t) => { let elev = new Eleventy("./test/stubs-1325", "./test/stubs-1325/_site"); elev.setFormats("11ty.js,js"); elev.disableLogger(); await elev.init(); await elev.eleventyFiles.getFiles(); await elev.startWatch(); let paths = await elev.eleventyFiles.getWatchPathCache(); await elev.stopWatch(); t.deepEqual(paths, ["./test/stubs-1325/test.11ty.js"]); }); test("Eleventy file watching (no JS dependencies)", async (t) => { let elev = new Eleventy("./test/stubs", "./test/stubs/_site", { config: eleventyConfig => { eleventyConfig.setWatchJavaScriptDependencies(false); } }); elev.setFormats("njk"); elev.disableLogger(); await elev.init(); await elev.startWatch(); let { targets, ignores } = await elev.getWatchedTargets(); await elev.stopWatch(); t.deepEqual(targets, [ "./package.json", "./test/stubs/**/*.njk", "./test/stubs/_includes/**", "./test/stubs/_data/**", "./.gitignore", "./.eleventyignore", "./test/stubs/.eleventyignore", `./test/stubs/**/*.{json,11tydata.mjs,11tydata.cjs,11tydata.js${isTypeScriptSupported() ? ",11tydata.mts,11tydata.cts,11tydata.ts" : ""}}`, ]); t.true(ignores.includes("node_modules/**")); t.true(ignores.includes("**/node_modules/**")); t.true(ignores.includes("test/stubs/_site/**")); t.true(ignores.includes("./.git/**")); t.true(ignores.includes(".cache")); }); test("Eleventy set input/output, one file input", async (t) => { let elev = new Eleventy("./test/stubs/index.html", "./test/stubs/_site"); t.is(elev.input, "./test/stubs/index.html"); t.is(elev.inputFile, "./test/stubs/index.html"); t.is(elev.inputDir, "./test/stubs/"); t.is(elev.outputDir, "./test/stubs/_site/"); }); test("Eleventy set input/output, one file input, deeper subdirectory", async (t) => { let elev = new Eleventy("./test/stubs/subdir/index.html", "./test/stubs/_site", { inputDir: "./test/stubs" }); t.is(elev.input, "./test/stubs/subdir/index.html"); t.is(elev.inputFile, "./test/stubs/subdir/index.html"); t.is(elev.inputDir, "./test/stubs/"); t.is(elev.outputDir, "./test/stubs/_site/"); }); test("Eleventy set input/output, one file input root dir", async (t) => { let elev = new Eleventy("./README.md", "./test/stubs/_site"); t.is(elev.input, "./README.md"); t.is(elev.inputFile, "./README.md"); t.is(elev.inputDir, "./"); t.is(elev.outputDir, "./test/stubs/_site/"); }); test("Eleventy set input/output, one file input root dir without leading dot/slash", async (t) => { let elev = new Eleventy("README.md", "./test/stubs/_site"); t.is(elev.input, "./README.md"); t.is(elev.inputDir, "./"); t.is(elev.outputDir, "./test/stubs/_site/"); }); test("Eleventy set input/output, one file input exitCode (script)", async (t) => { let previousExitCode = process.exitCode; let elev = new Eleventy("./test/stubs/exitCode/failure.njk", "./test/stubs/exitCode/_site", { source: "script", }); elev.disableLogger(); await t.throwsAsync(async () => { await elev.write(); }); // no change to the exit code when running script t.is(process.exitCode, previousExitCode); }); test("Eleventy set input/output, one file input exitCode (cli)", async (t) => { let previousExitCode = process.exitCode; let elev = new Eleventy("./test/stubs/exitCode/failure.njk", "./test/stubs/exitCode/_site", { source: "cli", }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.write()); t.is(e.message, "Having trouble rendering njk template ./test/stubs/exitCode/failure.njk"); t.is(process.exitCode, 1); process.exitCode = previousExitCode; }); test("Eleventy to json", async (t) => { let elev = new Eleventy("./test/stubs--to/"); elev.setIsVerbose(false); let result = await elev.toJSON(); t.deepEqual( result.filter((entry) => entry.url === "/test/"), [ { url: "/test/", inputPath: "./test/stubs--to/test.md", outputPath: "./_site/test/index.html", rawInput: localizeNewLines("# hi\n"), content: "

hi

\n", }, ] ); t.deepEqual( result.filter((entry) => entry.url === "/test2/"), [ { url: "/test2/", inputPath: "./test/stubs--to/test2.liquid", outputPath: "./_site/test2/index.html", rawInput: "{{ hi }}", content: "hello", }, ] ); }); test("Two Eleventies, two configs!!! (config used to be a global)", async (t) => { let elev1 = new Eleventy(); await elev1.initializeConfig(); t.is(elev1.eleventyConfig, elev1.eleventyConfig); t.is(elev1.config, elev1.config); delete elev1.config.uses; t.is(JSON.stringify(elev1.config), JSON.stringify(elev1.config)); let elev2 = new Eleventy(); await elev2.initializeConfig(); t.not(elev1.eleventyConfig, elev2.eleventyConfig); elev1.config.benchmarkManager = null; elev2.config.benchmarkManager = null; delete elev2.config.uses; t.is(JSON.stringify(elev1.config), JSON.stringify(elev2.config)); }); test("Config propagates to other instances correctly", async (t) => { let elev = new Eleventy(); await elev.init(); t.is(elev.eleventyServe.config, elev.config); t.is(elev.extensionMap.templateConfig, elev.eleventyConfig); t.is(elev.passthroughManager.templateConfig, elev.eleventyConfig); t.is(elev.eleventyFiles.templateConfig, elev.eleventyConfig); t.is(elev.templateData.templateConfig, elev.eleventyConfig); t.is(elev.writer.templateConfig, elev.eleventyConfig); }); test("Eleventy programmatic API without init", async (t) => { let elev = new Eleventy("./test/stubs--to/"); elev.setIsVerbose(false); let result = await elev.toJSON(); t.deepEqual( result.filter((entry) => entry.url === "/test/"), [ { url: "/test/", inputPath: "./test/stubs--to/test.md", outputPath: "./_site/test/index.html", rawInput: localizeNewLines("# hi\n"), content: "

hi

\n", }, ] ); t.deepEqual( result.filter((entry) => entry.url === "/test2/"), [ { url: "/test2/", inputPath: "./test/stubs--to/test2.liquid", outputPath: "./_site/test2/index.html", rawInput: `{{ hi }}`, content: "hello", }, ] ); }); test("Can Eleventy run two executeBuilds in parallel?", async (t) => { let elev = new Eleventy("./test/stubs--to/"); elev.setIsVerbose(false); let p1 = elev.toJSON(); let p2 = elev.toJSON(); let [result1, result2] = await Promise.all([p1, p2]); let test1Result = [ { url: "/test/", inputPath: "./test/stubs--to/test.md", outputPath: "./_site/test/index.html", rawInput: localizeNewLines("# hi\n"), content: "

hi

\n", }, ]; let test2Result = [ { url: "/test2/", inputPath: "./test/stubs--to/test2.liquid", outputPath: "./_site/test2/index.html", rawInput: "{{ hi }}", content: "hello", }, ]; t.deepEqual( result1.filter((entry) => entry.url === "/test/"), test1Result ); t.deepEqual( result1.filter((entry) => entry.url === "/test2/"), test2Result ); t.deepEqual( result2.filter((entry) => entry.url === "/test/"), test1Result ); t.deepEqual( result2.filter((entry) => entry.url === "/test2/"), test2Result ); }); test("Unicode in front matter `tags`, issue #670", async (t) => { let elev = new Eleventy("./test/stubs-670/", "./test/stubs-670/_site"); let results = await elev.toJSON(); results.sort((a, b) => { if (a.inputPath > b.inputPath) { return -1; } return 1; }); t.is(results[0].content.trim(), "2,Cañon City,all,"); }); test("#142: date 'git Last Modified' populates page.date", async (t) => { let elev = new Eleventy("./test/stubs-142/", "./test/stubs-142/_site"); let results = await elev.toJSON(); let [result] = results; // Warning: this doesn’t test the validity of the function, only that it populates page.date. let timestamp = await getUpdatedTimestamp("./test/stubs-142/index.njk"); t.truthy(result.content.trim()); t.truthy(timestamp); t.is(result.content.trim(), "" + timestamp); }); test("git getUpdatedTimestamp returns undefined on nonexistent path", async (t) => { t.is(await getUpdatedTimestamp("./test/invalid.invalid"), undefined); }); test("#2167: Pagination with permalink: false", async (t) => { let elev = new Eleventy("./test/stubs-2167/", "./test/stubs-2167/_site"); elev.disableLogger(); elev.setDryRun(true); let [,pages] = await elev.write(); t.is(pages.length, 0); let results = await elev.toJSON(); t.is(results.length, 5); }); test("Pagination over collection using eleventyComputed (liquid)", async (t) => { t.plan(5); let elev = new Eleventy( "./test/stubs-pagination-computed-quotes/", "./test/stubs-pagination-computed-quotes/_site", { config: function (eleventyConfig) { eleventyConfig.addFilter("selectRandomFromArray", (arr) => { t.true(Array.isArray(arr)); t.deepEqual(arr, ["The person that shared this is awesome"]); return arr[0]; }); }, } ); let results = await elev.toJSON(); t.is(results.length, 2); let content = results.map((entry) => entry.content).sort(); t.is(content[0], "No"); t.is(content[1], "The person that shared this is awesome"); }); test("Pagination over collection using eleventyComputed (njk)", async (t) => { t.plan(5); let elev = new Eleventy( "./test/stubs-pagination-computed-quotes-njk/", "./test/stubs-pagination-computed-quotes-njk/_site", { config: function (eleventyConfig) { eleventyConfig.addFilter("selectRandomFromArray", (arr) => { t.true(Array.isArray(arr)); t.deepEqual(arr, ["The person that shared this is awesome"]); return arr[0]; }); }, } ); let results = await elev.toJSON(); t.is(results.length, 2); let content = results.map((entry) => entry.content).sort(); t.is(content[0], "No"); t.is(content[1], "The person that shared this is awesome"); }); test("Paginated template uses proxy and global data", async (t) => { let elev = new Eleventy( "./test/proxy-pagination-globaldata/", "./test/proxy-pagination-globaldata/_site", { config: function (eleventyConfig) {}, } ); let results = await elev.toJSON(); let allContentMatches = results.filter((entry) => { return entry.content.trim() === "BANNER TEXT"; }); t.is(results.length, allContentMatches.length); }); test("Liquid shortcode with multiple arguments(issue #2348)", async (t) => { // NOTE issue #2348 was only active when you were processing multiple templates at the same time. let elev = new Eleventy("./test/stubs-2367/", "./test/stubs-2367/_site", { config: function (eleventyConfig) { eleventyConfig.addShortcode("simplelink", function (...args) { return JSON.stringify(args); }); }, }); let arr = ["layout", "/mylayout", "layout", "/mylayout", "layout", "/mylayout"]; let str = normalizeNewLines(`${JSON.stringify(arr)} ${JSON.stringify(arr)}`); let results = await elev.toJSON(); t.is(results.length, 2); let content = results.map((entry) => entry.content).sort(); t.is(normalizeNewLines(content[0]), str); t.is(normalizeNewLines(content[1]), str); }); test("#2224: date 'git created' populates page.date", async (t) => { let elev = new Eleventy("./test/stubs-2224/", "./test/stubs-2224/_site"); let results = await elev.toJSON(); let [result] = results; // This doesn’t test the validity of the function, only that it populates page.date. let timestamp = await getCreatedTimestamp("./test/stubs-2224/index.njk"); t.truthy(result.content.trim()); t.truthy(timestamp); t.is(result.content.trim(), "" + timestamp); }); test("git getCreatedTimestamp returns undefined on nonexistent path", async (t) => { t.is(await getCreatedTimestamp("./test/invalid.invalid"), undefined); }); test("Does pathPrefix affect page URLs", async (t) => { let elev = new Eleventy("./README.md", "./_site", { config: function (eleventyConfig) { return { pathPrefix: "/testdirectory/", }; }, }); let results = await elev.toJSON(); let [result] = results; t.is(result.url, "/README/"); }); test("Improvements to custom template syntax APIs (includes a layout file) #2258", async (t) => { let elev = new Eleventy("./test/stubs-2258/", "./test/stubs-2258/_site", { configPath: "./test/stubs-2258/eleventy.config.cjs", }); // Restore previous contents let includeFilePath = "./test/stubs-2258/_includes/_code.scss"; let previousContents = `code { padding: 0.25em; line-height: 0; }`; let newContents = `/* New content */`; fs.writeFileSync(includeFilePath, previousContents, "utf8"); let sizes = [TemplateContent._inputCache.size, TemplateContent._compileCache.size]; let results = await elev.toJSON(); t.is(results.length, 1); t.is( normalizeNewLines(results[0].content), `/* Banner */ ${previousContents} /* Comment */` ); // Cache sizes are now one bigger t.is(sizes[0] + 1, 1); t.is(sizes[1] + 1, 1); let results2 = await elev.toJSON(); t.is( normalizeNewLines(results2[0].content), `/* Banner */ ${previousContents} /* Comment */` ); // Cache sizes are unchanged from last build t.is(sizes[0] + 1, 1); t.is(sizes[1] + 1, 1); fs.writeFileSync(includeFilePath, newContents, "utf8"); // This also triggers that the file has changed in the event bus via setPreviousBuildModifiedFile elev.setIncrementalFile(includeFilePath); let results3 = await elev.toJSON(); t.is( normalizeNewLines(results3[0].content), `/* Banner */ ${newContents} /* Comment */` ); fs.writeFileSync(includeFilePath, previousContents, "utf8"); }); test("`useLayouts: false` custom engine property #2830", async (t) => { let elev = new Eleventy("./test/stubs-2258-2830-skip-layouts/", "./test/stubs-2258-2830-skip-layouts/_site", { configPath: "./test/stubs-2258-2830-skip-layouts/eleventy.config.cjs", }); let results = await elev.toJSON(); t.is(results.length, 1); t.is( normalizeNewLines(results[0].content), `code { padding: 0.25em; line-height: 0; }` ); }); test("Lodash get (for pagination data target) object key with spaces, issue #2851", (t) => { let data = { collections: { "tag with spaces": 2, }, }; t.is(2, lodashGet(data, "collections['tag with spaces']")); // wow, this works huh? t.is(2, lodashGet(data, "collections.tag with spaces")); let tm = new TemplateMap(new TemplateConfig()); t.is(tm.getTagTarget("collections.tag with spaces"), "tag with spaces"); t.is(tm.getTagTarget("collections['tag with spaces']"), "tag with spaces"); t.is(tm.getTagTarget('collections["tag with spaces"]'), "tag with spaces"); }); test("Eleventy tag collection with spaces in the tag name, issue #2851", async (t) => { let elev = new Eleventy("./test/stubs-2851", "./test/stubs-2851/_site", { config: function (eleventyConfig) { eleventyConfig.dataFilterSelectors.add("collections"); }, }); elev.setIsVerbose(false); let result = await elev.toJSON(); t.deepEqual(result.length, 2); t.deepEqual(result.length, result[0].data.collections.all.length); t.deepEqual(result[0].data.collections["tag with spaces"].length, 1); }); test("this.eleventy on JavaScript template functions, issue #2790", async (t) => { t.plan(3); let elev = new Eleventy("./test/stubs-2790", "./test/stubs-2790/_site", { config: function (eleventyConfig) { eleventyConfig.addJavaScriptFunction("jsfunction", function () { t.truthy(this.eleventy); return this.eleventy.generator.split(" ")[0]; }); }, }); let result = await elev.toJSON(); t.deepEqual(result.length, 1); t.deepEqual(result[0].content, `

Eleventy

`); }); test("Global data JS files should only execute once, issue #2753", async (t) => { let elev = new Eleventy("./test/stubs-2753", "./test/stubs-2753/_site", { config: function (eleventyConfig) {}, }); let result = await elev.toJSON(); t.deepEqual(result.length, 2); t.deepEqual(result[0].content, `1`); t.deepEqual(result[0].content, `1`); }); function sortResultsBy(results, key = "content") { results.sort((a, b) => { if(a[key] < b[key]) { return -1; } if(b[key] < a[key]) { return 1; } return 0; }); } test("Access to raw input of file (toJSON), issue #1206", async (t) => { let elev = new Eleventy("./test/stubs-1206", "./test/stubs-1206/_site", { config: function (eleventyConfig) {}, }); let results = await elev.toJSON(); sortResultsBy(results, "content"); t.deepEqual(results.length, 2); t.deepEqual(results[0].content, `This is the first template.This is the first template.{{ page.rawInput }}`); t.deepEqual(results[0].rawInput, `This is the first template.{{ page.rawInput }}`); t.deepEqual(results[1].content, `This is the second template.This is the first template.{{ page.rawInput }}`); t.deepEqual(results[1].rawInput, `This is the second template.{{ collections.tag1[0].rawInput }}`); }); // Warning: this test writes to the file system test("Access to raw input of file (dryRun), issue #1206", async (t) => { let elev = new Eleventy("./test/stubs-1206", "./test/stubs-1206/_site", { config: function (eleventyConfig) {}, }); elev.disableLogger(); let [,results] = await elev.write(); sortResultsBy(results, "content"); t.deepEqual(results.length, 2); t.deepEqual(results[0].content, `This is the first template.This is the first template.{{ page.rawInput }}`); t.deepEqual(results[0].rawInput, `This is the first template.{{ page.rawInput }}`); t.deepEqual(results[1].content, `This is the second template.This is the first template.{{ page.rawInput }}`); t.deepEqual(results[1].rawInput, `This is the second template.{{ collections.tag1[0].rawInput }}`); deleteDirectory("./test/stubs-1206/_site/"); }); test("eleventy.before and eleventy.after Event Arguments, directories", async (t) => { t.plan(6); let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.on("eleventy.before", arg => { t.is(arg.inputDir, "./test/noop/"); t.is(arg.directories.input, "./test/noop/"); t.is(arg.directories.includes, "./test/noop/_includes/"); }) eleventyConfig.on("eleventy.after", arg => { t.is(arg.inputDir, "./test/noop/"); t.is(arg.directories.input, "./test/noop/"); t.is(arg.directories.includes, "./test/noop/_includes/"); }) }, }); let results = await elev.toJSON(); }); test("eleventy.after fires sequentially setting eventEmitterMode 'sequential'", async (t) => { let reachFirst; const firstReached = new Promise(resolve => reachFirst = resolve) let next; const firstResult = new Promise(resolve => next = resolve) let secondCalled = false; let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.setEventEmitterMode('sequential') eleventyConfig.on("eleventy.after", arg => { reachFirst() return firstResult; }) eleventyConfig.on("eleventy.after", arg => { secondCalled = true; }) }, }); const resultPromise = elev.toJSON(); await firstReached; t.is(secondCalled, false) next() await 'microtask' t.is(secondCalled, true) await resultPromise; }) test("setInputDirectory config method #1503", async (t) => { t.plan(5); let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.setInputDirectory("./test/noop2/"); eleventyConfig.on("eleventy.before", arg => { t.is(arg.directories.input, "./test/noop2/"); t.is(arg.directories.includes, "./test/noop2/_includes/"); t.is(arg.directories.data, "./test/noop2/_data/"); t.is(arg.directories.layouts, undefined); t.is(arg.directories.output, "./test/noop/_site/"); }) }, }); let results = await elev.toJSON(); }); test("setIncludesDirectory config method #1503", async (t) => { t.plan(5); let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.setIncludesDirectory("myincludes"); eleventyConfig.on("eleventy.before", arg => { t.is(arg.directories.input, "./test/noop/"); t.is(arg.directories.includes, "./test/noop/myincludes/"); t.is(arg.directories.data, "./test/noop/_data/"); t.is(arg.directories.layouts, undefined); t.is(arg.directories.output, "./test/noop/_site/"); }) }, }); let results = await elev.toJSON(); }); test("setDataDirectory config method #1503", async (t) => { t.plan(5); let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.setDataDirectory("data"); eleventyConfig.on("eleventy.before", arg => { t.is(arg.directories.input, "./test/noop/"); t.is(arg.directories.includes, "./test/noop/_includes/"); t.is(arg.directories.data, "./test/noop/data/"); t.is(arg.directories.layouts, undefined); t.is(arg.directories.output, "./test/noop/_site/"); }) }, }); let results = await elev.toJSON(); }); test("setLayoutsDirectory config method #1503", async (t) => { t.plan(5); let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.setLayoutsDirectory("layouts"); eleventyConfig.on("eleventy.before", arg => { t.is(arg.directories.input, "./test/noop/"); t.is(arg.directories.includes, "./test/noop/_includes/"); t.is(arg.directories.data, "./test/noop/_data/"); t.is(arg.directories.layouts, "./test/noop/layouts/"); t.is(arg.directories.output, "./test/noop/_site/"); }) }, }); let results = await elev.toJSON(); }); test("setInputDirectory config method #1503 in a plugin throws error", async (t) => { let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(() => { eleventyConfig.setInputDirectory("./test/noop2/"); }); }, }); await t.throwsAsync(() => elev.toJSON(), { // The `set*Directory` configuration API methods are not yet allowed in plugins. message: "Error processing a plugin", }); }); test("Accepts absolute paths for input and output", async (t) => { let input = path.resolve("./test/noop/"); let output = path.resolve("./test/noop/_site"); let elev = new Eleventy(input, output); let results = await elev.toJSON(); // trailing slashes are expected t.is(PathNormalizer.normalizeSeperator(elev.directories.input), PathNormalizer.normalizeSeperator("./test/noop/")); t.is(PathNormalizer.normalizeSeperator(elev.directories.includes), PathNormalizer.normalizeSeperator("./test/noop/_includes/")); t.is(PathNormalizer.normalizeSeperator(elev.directories.data), PathNormalizer.normalizeSeperator("./test/noop/_data/")); t.is(elev.directories.layouts, undefined); t.is(PathNormalizer.normalizeSeperator(elev.directories.output), PathNormalizer.normalizeSeperator("./test/noop/_site/")); }); test("Accepts absolute paths urls for input and output, results output #3805", async (t) => { let input = path.resolve("./test/stubs-absolute/test.md"); let output = path.resolve("./test/stubs-absolute/_site"); let elev = new Eleventy(input, output); let results = await elev.toJSON(); t.is(results.length, 1); }); test("Accepts absolute paths urls for input and output and a virtual template, results output #3805", async (t) => { let input = path.resolve("./test/noop/"); let output = path.resolve("./test/noop/_site"); let elev = new Eleventy(input, output, { config: eleventyConfig => { eleventyConfig.addTemplate("index.md", `# Title`) } }); let results = await elev.toJSON(); t.is(results.length, 1); }); test("Eleventy config export (ESM)", async (t) => { t.plan(5); let elev = new Eleventy("test/stubs/cfg-directories-export", null, { configPath: "./test/stubs/cfg-directories-export/eleventy.config.js", config: function (eleventyConfig) { eleventyConfig.on("eleventy.after", arg => { t.is(arg.directories.input, "./src/"); t.is(arg.directories.includes, "./src/myincludes/"); t.is(arg.directories.data, "./src/mydata/"); t.is(arg.directories.layouts, undefined); t.is(arg.directories.output, "./dist/"); }) }, }); let result = await elev.toJSON(); }); test("Eleventy config export (CommonJS)", async (t) => { t.plan(5); let elev = new Eleventy("test/stubs/cfg-directories-export-cjs", null, { configPath: "./test/stubs/cfg-directories-export-cjs/eleventy.config.cjs", config: function (eleventyConfig) { eleventyConfig.on("eleventy.after", arg => { t.is(arg.directories.input, "./src/"); t.is(arg.directories.includes, "./src/myincludes2/"); t.is(arg.directories.data, "./src/mydata2/"); t.is(arg.directories.layouts, undefined); t.is(arg.directories.output, "./dist2/"); }) }, }); let result = await elev.toJSON(); }); test("Eleventy setting reserved data throws error (eleventy)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- eleventy: key1: NOOOOO ---`); } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); t.is(e.cause.toString(), "TypeError: Cannot add property key1, object is not extensible"); }); test("Eleventy setting reserved data throws error (pkg)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- pkg: myOwn: OVERRIDE ---`); } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); t.is(e.cause.toString(), "TypeError: Cannot add property myOwn, object is not extensible"); }); test("Eleventy pagination works okay with reserved data throws (eleventy) Issue #3262", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- pagination: data: "test" size: 1 test: - a - b - c --- {{ eleventy.generator }}`); } }); elev.disableLogger(); let result = await elev.toJSON(); t.is(result.length, 3); }); test("Eleventy setting reserved data throws error (page)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- page: "My page value" ---`) } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: page. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy setting reserved data throws error (content)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- content: "My page value" ---`) } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: content. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy setting reserved data throws error (collections)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- collections: [] ---`) } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: collections. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy setting pkg data is okay when pkg is remapped to parkour", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- pkg: myOwn: OVERRIDE ---`); } }); elev.disableLogger(); await elev.initializeConfig({ keys: { package: "parkour" } }); // Remap successful t.is(elev.eleventyConfig.config.keys.package, "parkour"); let [result] = await elev.toJSON(); t.deepEqual(result, { content: "", inputPath: "./test/stubs-virtual/index.html", outputPath: "./_site/index.html", rawInput: "", url: "/" }); }); test("Eleventy setting pkg data is okay when keys.package is false", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- pkg: myOwn: OVERRIDE --- {{ pkg.myOwn }}`); } }); elev.disableLogger(); await elev.initializeConfig({ keys: { package: false } }); // Remap successful t.is(elev.eleventyConfig.config.keys.package, false); let [result] = await elev.toJSON(); t.deepEqual(result, { content: "OVERRIDE", inputPath: "./test/stubs-virtual/index.html", outputPath: "./_site/index.html", rawInput: "{{ pkg.myOwn }}", url: "/" }); }); test("Eleventy setting reserved data throws error (pkg remapped to parkour)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `--- parkour: myOwn: OVERRIDE ---`); } }); elev.disableLogger(); await elev.initializeConfig({ keys: { package: "parkour" } }); // Remap successful t.is(elev.eleventyConfig.config.keys.package, "parkour"); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names. You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); t.is(e.cause.toString(), "TypeError: Cannot add property myOwn, object is not extensible"); }); test("Eleventy data schema (success) #879", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index1.html", "", { draft: true, eleventyDataSchema: function(data) { if(typeof data.draft !== "boolean") { throw new Error("Invalid data type for draft."); } } }); eleventyConfig.addTemplate("index2.html", "", { draft: true, eleventyDataSchema: function(data) { if(typeof data.draft !== "boolean") { throw new Error("Invalid data type for draft."); } } }); } }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 2); }); test("Eleventy data schema (fails) #879", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index1.html", "", { draft: 1, eleventyDataSchema: function(data) { if(typeof data.draft !== "boolean") { throw new Error("Invalid data type for draft."); } } }); } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'Error in the data schema for: ./test/stubs-virtual/index1.html (via `eleventyDataSchema`)' }); t.is(e.cause.toString(), "Error: Invalid data type for draft."); }); test("Eleventy data schema (fails, using zod) #879", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index1.html", "", { draft: 1, eleventyDataSchema: function(data) { let result = z.object({ draft: z.boolean().or(z.undefined()), }).safeParse(data); if(result.error) { throw fromZodError(result.error); } } }); } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON(), { message: 'Error in the data schema for: ./test/stubs-virtual/index1.html (via `eleventyDataSchema`)' }); t.is(e.cause.toString(), 'Validation error: Invalid input: expected boolean, received number at "draft" or Invalid input: expected undefined, received number at "draft"'); }); test("Eleventy data schema has access to custom collections created via API #879", async (t) => { t.plan(2); let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addCollection("userCollection", function (collection) { return collection.getAll(); }); eleventyConfig.addTemplate("index1.html", "", { eleventyDataSchema: function(data) { t.is(data.collections.userCollection.length, 1); } }); } }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); }); test("Eleventy transforms filter (using collections and page override data)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { pathPrefix: "hi", config: eleventyConfig => { eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addTemplate("index.html", `abc`, { tags: "posts" }); eleventyConfig.addTemplate("feed.njk", `{% for post in collections.posts %}{{ post.content | renderTransforms(post.page) | safe }}{% endfor %}`, { permalink: "feed.xml" }); } }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 2); t.is(results.filter(({inputPath}) => inputPath.endsWith("index.html"))[0].content, `abc`); t.is(results.filter(({inputPath}) => inputPath.endsWith("feed.njk"))[0].content, `abc`); }); test("Custom Markdown Render with permalink, Issue #2780", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addExtension("md", { compile: str => { return data => marked.parse(str); } }); eleventyConfig.addTemplate("template.md", `# Markdown?`, { permalink: "/permalink.html" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/permalink.html`); t.is(results[0].content.trim(), `

Markdown?

`); }); test("Custom Markdown Render with permalink, Issue #2780 #3339", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplateFormats("markdown"); eleventyConfig.addExtension("markdown", { key: "md" }); eleventyConfig.addTemplate("filename-hi.markdown", `# Markdown?`, { permalink: "/{{ page.fileSlug }}.html" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/filename-hi.html`); t.is(results[0].content.trim(), `

Markdown?

`); }); test("Test input/output conflicts (input overwrites output), Issue #3327", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`, { permalink: "test.html" }); } }); elev.disableLogger(); let e = await t.throwsAsync(async () => { await elev.toJSON(); }); t.true(e.toString().startsWith("DuplicatePermalinkOutputError:")); }); test("Test input/output conflicts (output overwrites another input), Issue #3327", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", { config: eleventyConfig => { eleventyConfig.addTemplate("test.html", `# Markdown`); eleventyConfig.addTemplate("index.html", `# Markdown`, { permalink: "test.html" }); } }); elev.disableLogger(); let e = await t.throwsAsync(async () => { await elev.toJSON(); }); t.true(e.toString().startsWith("DuplicatePermalinkOutputError:")); }); test("Eleventy data schema has access to custom collections created via API #613 #3345", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addCollection("userCollection", async function (collection) { let c = collection.getFilteredByTag("posts"); for(let item of c) { const frontMatter = await item.template.read(); frontMatter.content = `lol\n${frontMatter.content}`; item.template.frontMatter = frontMatter; } return c; }); eleventyConfig.addTemplate("home.html", "{% for post in collections.userCollection %}{{ post.content }}{% endfor %}"); eleventyConfig.addTemplate("post.html", "test", { tags: "posts" }); } }); elev.disableLogger(); let results = await elev.toJSON(); let [home] = results.filter(item => item.url.endsWith("/home/")); t.truthy(home); t.is(home.content, "test"); }); test("Custom Nunjucks syntax has shortcode with access to `this`, Issue #3310", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addShortcode("customized", function(argString) { return `${this.page.url}:${argString}:Custom Shortcode`; }); let njkEnv = new nunjucks.Environment(); function CustomExtension() { this.tags = ['customized']; this.parse = function(parser, nodes, lexer) { let args; let tok = parser.nextToken(); args = parser.parseSignature(true, true); parser.advanceAfterBlockEnd(tok.value); return new nodes.CallExtension(this, "run", args); }; this.run = function(context, argString) { let fn = eleventyConfig.augmentFunctionContext( eleventyConfig.getShortcode("customized"), { source: context.ctx, // lazy: false, // getter: (key, context) => context?.[key]; // overwrite: true, } ); return fn(argString); }; } njkEnv.addExtension('CustomExtension', new CustomExtension()); eleventyConfig.addTemplateFormats("njknew"); eleventyConfig.addExtension("njknew", { compile: (str, inputPath) => { let tmpl = new nunjucks.Template(str, njkEnv, inputPath, false); return function(data) { return new Promise(function (resolve, reject) { tmpl.render(data, function (err, res) { if (err) { reject(err); } else { resolve(res); } }); }); } } }); eleventyConfig.addTemplate("template.njknew", `

{{ hello }}:{% customized "passed in" %}

`, { hello: "goodbye" }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `

goodbye:/template/:passed in:Custom Shortcode

`); }); test("Related to issue 3206: Does Nunjucks throwOnUndefined variables require normalizeContext to be a lazy get", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addShortcode("customized", function(argString) { return `${this.page.url}:Custom Shortcode`; }); eleventyConfig.setNunjucksEnvironmentOptions({ throwOnUndefined: true, }); eleventyConfig.addTemplate("index.html", `HELLO{% customized %}:{{ page.url }}`); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `HELLO/:Custom Shortcode:/`); }); test("#727: Error messaging when trying to use a missing layout", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.html", `HELLO {{ page.url }}`, { layout: "does-not-exist.html", }); } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: `Problem creating an Eleventy Layout for the "./test/stubs-virtual/index.html" template file.` }); }); test("#1419: Shortcode in a permalink", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addShortcode("shortcode", () => "url-slug"); eleventyConfig.addTemplate("index.njk", "", { permalink: "/{% shortcode %}/", }); } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, `/url-slug/`); }); test("#3373: Throw an error when explicit config path is not found.", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { configPath: "this-file-is-not-found.js" }); let e = await t.throwsAsync(() => elev.toJSON()); t.is(e.message, "A configuration file was specified but not found: this-file-is-not-found.js"); }); test("Eleventy loader can force ESM mode", async (t) => { let elev = new Eleventy("./README.md", "./_site", { loader: "esm", }); t.is(elev.isEsm, true); }); test("Eleventy loader can force CommonJS mode", async (t) => { let elev = new Eleventy("./README.md", "./_site", { loader: "cjs", }); t.is(elev.isEsm, false); }); test("Truthy outputPath without a file extension now throws an error, issue #3399", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { // eleventyConfig.configureErrorReporting({ allowMissingExtensions: true }); eleventyConfig.addTemplate("index.html", "", { permalink: "foo" }) }, }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { // The `set*Directory` configuration API methods are not yet allowed in plugins. message: `The template at './test/stubs-virtual/index.html' attempted to write to './_site/foo' (via \`permalink\` value: 'foo'), which is a target on the file system that does not include a file extension. You *probably* want to add a file extension to your permalink so that hosts will know how to correctly serve this file to web browsers. Without a file extension, this file may not be reliably deployed without additional hosting configuration (it won’t have a mime type) and may also cause local development issues if you later attempt to write to a subdirectory of the same name. Learn more: https://v3.11ty.dev/docs/permalinks/#trailing-slashes This is usually but not *always* an error so if you’d like to disable this error message, add \`eleventyAllowMissingExtension: true\` somewhere in the data cascade for this template or use \`eleventyConfig.configureErrorReporting({ allowMissingExtensions: true });\` to disable this feature globally.` }); }); test("Truthy outputPath without a file extension can be ignored, issue #3399", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { // eleventyConfig.configureErrorReporting({ allowMissingExtensions: true }); eleventyConfig.addTemplate("index.html", "", { permalink: "foo", eleventyAllowMissingExtension: true }) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, "/foo"); }); test("Allow list for some file types without a file extension, issue #3399", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addTemplate("index.html", "", { permalink: "/test/_redirects" }) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].url, "/test/_redirects"); }); test("Truthy outputPath without a file extension error message is disabled, issue #3399", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.configureErrorReporting({ allowMissingExtensions: true }); eleventyConfig.addTemplate("index.html", "", { permalink: "foo" }) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); }); test("permalink: false outputPath new error message won’t throw an error, issue #3399", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addTemplate("index.html", "", { permalink: false }) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); }); test("permalink on custom template lang, issue #3619", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.addGlobalData("permalink", () => { return (data) => `/rewrite/${data.page.filePathStem}.${data.page.outputFileExtension}`; }); eleventyConfig.addTemplateFormats("scss"); eleventyConfig.addExtension("scss", { outputFileExtension: "css", compileOptions: { permalink(inputContent, inputPath) { return (data) => { return `/testing/${data.permalink(data)}`; } } }, compile: function (str, inputPath) { // TODO declare data variables as SASS variables? return async function (data) { return new Promise(function (resolve, reject) { sass.render( { data: str, outFile: "test_this_is_to_not_write_a_file.css", }, function (error, result) { if (error) { reject(error); } else { resolve(result.css.toString("utf8")); } }, ); }); }; }, }); eleventyConfig.addTemplate("index.scss", `html { color: red; }`) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results[0].url, "/testing/rewrite/index.css"); t.is(results[0].content, `html { color: red; }`); }); test("Template data throws error when tags is not an Array or String #1791", async (t) => { let elev = new Eleventy("./test/noop/", "./test/noop/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("index.html", "", { tags: {"one": 1, "two": 2} }); }, }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { // The `set*Directory` configuration API methods are not yet allowed in plugins. message: "String or Array expected for `tags` in virtual template: ./test/noop/index.html. Received: { one: 1, two: 2 }", }); }); test("sass docs on 11ty.dev, issue #408", async (t) => { let elev = new Eleventy("./test/stubs-408-sass/", undefined, { config: function (eleventyConfig) { eleventyConfig.addTemplateFormats("scss"); eleventyConfig.addExtension("scss", { outputFileExtension: "css", // opt-out of Eleventy Layouts useLayouts: false, compile: async function (inputContent, inputPath) { let parsed = path.parse(inputPath); if(parsed.name.startsWith("_")) { return; } let result = sass.compileString(inputContent, { loadPaths: [ parsed.dir || ".", this.config.dir.includes ] }); this.addDependencies(inputPath, result.loadedUrls); return async (data) => { return result.css; }; }, }); }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 2); let code = results.filter(entry => entry.inputPath.endsWith("_code.scss"))[0]; t.is(code.url, "/_code.css"); t.is(code.content, undefined); let main = results.filter(entry => entry.inputPath.endsWith("style.scss"))[0]; t.is(main.url, "/style.css"); t.is( normalizeNewLines(main.content), `code { padding: 0.25em; line-height: 0; } /* Comment */`); }); test("Use a date object for `date`, issue #3022", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addTemplate("index.html", "", { date: new Date() }) }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); t.truthy(results[0].data.page.date instanceof Date); }); test("Use a date object for `date` (js object front matter), issue #3022", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addTemplate("index.html", `---js { date: new Date(), } ---`); }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); t.truthy(results[0].data.page.date instanceof Date); }); test("Use a date object for `date` (js front matter), issue #3022", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: function (eleventyConfig) { eleventyConfig.dataFilterSelectors.add("page.date"); eleventyConfig.addTemplate("index.html", `---js let date = new Date(); ---`); }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results.length, 1); t.truthy(results[0].data.page.date instanceof Date); }); test("Cleaner constructor args #3880", async (t) => { let elev = new Eleventy({ input: "./test/stubs-virtual/", config: eleventyConfig => { eleventyConfig.addTemplate("index.md", `# Title`) } }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content.trim(), `

Title

`); }); ================================================ FILE: test/EleventyVirtualTemplatesTest.js ================================================ import test from "ava"; import fs from "fs"; import { feedPlugin } from "@11ty/eleventy-plugin-rss"; import Eleventy from "../src/Eleventy.js"; import DuplicatePermalinkOutputError from "../src/Errors/DuplicatePermalinkOutputError.js"; import { deleteDirectory } from "./_testHelpers.js"; test("Virtual templates, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`) }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); }); test("Virtual templates with front matter, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("./virtual.md", `--- myKey: myValue --- # {{ myKey }}`) }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

myValue

`); t.deepEqual(results[0].rawInput, `# {{ myKey }}`); }); test("Virtual templates with supplemental data, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# {{ myKey }}`, { myKey: "myValue" }) }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

myValue

`); t.deepEqual(results[0].rawInput, `# {{ myKey }}`); }); // Supplemental data overrides front matter. test("Virtual templates with front matter and supplemental data, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `--- myKey1: myValue1 myKey3: myValueFm --- # {{ myKey1 }}{{ myKey2 }}{{ myKey3 }}`, { myKey2: "myValue2", myKey3: "myValueData" }) }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

myValue1myValue2myValueData

`); t.deepEqual(results[0].rawInput, `# {{ myKey1 }}{{ myKey2 }}{{ myKey3 }}`); }); test("Virtual template conflicts with file on file system, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs/stubs-virtual-conflict", "./test/stubs/stubs-virtual-conflict/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Virtual template`) }, }); elev.disableLogger(); await t.throwsAsync(elev.toJSON(), { message: `A virtual template had the same path as a file on the file system: "./test/stubs/stubs-virtual-conflict/virtual.md"` }); }); test("Virtual templates try to output to the same file, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual-one.md", "", { permalink: "/output.html" }) eleventyConfig.addTemplate("virtual-two.md", "", { permalink: "/output.html" }) }, }); elev.disableLogger(); await t.throwsAsync(elev.toJSON(), { instanceOf: DuplicatePermalinkOutputError, }); }); // Warning: this test writes to the file system test("Virtual template writes to file system, issue #1612", async (t) => { let elev = new Eleventy("./test/stubs-virtual", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`) }, }); elev.disableLogger(); let [,results] = await elev.write(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); t.true(fs.existsSync("./test/stubs-virtual/_site/virtual/index.html")); deleteDirectory("./test/stubs-virtual/_site/"); }); test("Virtual templates conflict", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`); eleventyConfig.addTemplate("virtual.md", `# Hello`); }, }); let e = await t.throwsAsync(async () => { await elev.toJSON(); }); t.is(e.message, "Virtual template conflict: you can’t add multiple virtual templates that have the same inputPath: virtual.md"); }); // https://github.com/11ty/eleventy-plugin-rss/issues/50 test("RSS virtual templates plugin", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`, { tag: "posts" }) eleventyConfig.addPlugin(feedPlugin, { type: "atom", // or "rss", "json" outputPath: "/feed.xml", collection: { name: "posts", // iterate over `collections.posts` limit: 10, // 0 means no limit }, }); }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 2); let [ feed ] = results.filter(entry => entry.outputPath.endsWith(".xml")); t.truthy(feed.content.startsWith(``)); }); test("Virtual templates as layouts, issue #2307", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`, { layout: "virtual.html" }); let layoutPath = eleventyConfig.directories.getLayoutPathRelativeToInputDirectory("virtual.html"); eleventyConfig.addTemplate(layoutPath, `{{ content }}`); }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); }); test("11ty.js Virtual Templates (object), issue #3347", async (t) => { let templateDefinition = { data: () => { return { var: 2 }; }, render: function(data) { return `this is a test ${data.var}.`; } }; let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", templateDefinition); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `this is a test 2.`); // TODO support rawInput on 11ty.js? Issue #3348 // t.deepEqual(results[0].rawInput.data, templateDefinition.data); // t.deepEqual(results[0].rawInput.render, templateDefinition.render); }); test("11ty.js Virtual Templates (function), issue #3347", async (t) => { let templateDefinition = function(data) { return `this is a test ${data.page.url}.`; }; let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", templateDefinition); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `this is a test /virtual/.`); // TODO support rawInput on 11ty.js? // t.deepEqual(results[0].rawInput, templateDefinition); }); test("11ty.js class templates with invalid signature, issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", class {}); } }); elev.disableLogger(); await t.throwsAsync(elev.toJSON(), { message: `Invalid class signature for an 11ty.js template: needs a render or data instance property.` }); }); test("11ty.js class templates with instance properties (data), issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", class { data() { return {} } }); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), ``); }); test("11ty.js class templates with instance properties (render), issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", class { render() { return "Hello!" } }); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `Hello!`); }); test("11ty.js class templates with instance properties (data and render), issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", class { data() { return { key: "world" }; } render(data) { return `Hello ${data.key}!` } }); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `Hello world!`); }); test("11ty.js class templates with instance properties (data and render arrows), issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", class { data = () => { return { key: "world" }; } render = (data) => { return `Hello ${data.key}!` } }); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `Hello world!`); }); test("11ty.js class templates with instance properties (data and render arrows, new), issue #1645", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.11ty.js", new class { data = () => { return { key: "world" }; } render = (data) => { return `Hello ${data.key}!` } }); } }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `Hello world!`); }); ================================================ FILE: test/ExistsCacheTest.js ================================================ import test from "ava"; import ExistsCache from "../src/Util/ExistsCache.js"; test("Simple check (with directory checking)", async t => { let cache = new ExistsCache(); t.is(cache.exists("test"), true); t.is(cache.size, 1); t.is(cache.lookupCount, 1); t.is(cache.exists("test"), true); t.is(cache.size, 1); t.is(cache.lookupCount, 1); t.is(cache.exists("test/stubs"), true); t.is(cache.size, 2); t.is(cache.lookupCount, 2); t.is(cache.exists("test/stubs/does-not-exist-ever-hslkadjflk"), false); t.is(cache.size, 3); t.is(cache.lookupCount, 3); }); test("Simple check (parent directory already invalidated)", async t => { let cache = new ExistsCache(); t.is(cache.exists("test/folder-does-not-exist-askdfjkladjs"), false); t.is(cache.size, 1); t.is(cache.lookupCount, 1); // we already know this *doesn’t* exist. t.is(cache.exists("test/folder-does-not-exist-askdfjkladjs/file-we-already-know-does-not-exist.liquid"), false); t.is(cache.size, 2); t.is(cache.lookupCount, 2); }); ================================================ FILE: test/FileSystemSearchTest.js ================================================ import test from "ava"; import FileSystemSearch from "../src/FileSystemSearch.js"; test("Base", async (t) => { let fs = new FileSystemSearch(); t.is(fs.count, 0); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), [ "./test/file-system-search/file.txt", ]); t.is(fs.count, 1); fs.add("./test/file-system-search/virtual-file.txt"); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), [ "./test/file-system-search/file.txt", "./test/file-system-search/virtual-file.txt", ]); t.is(fs.count, 1); fs.add("./test/file-system-search/another-file.txt"); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), [ "./test/file-system-search/file.txt", "./test/file-system-search/virtual-file.txt", "./test/file-system-search/another-file.txt", ]); t.is(fs.count, 1); // Delete fs.delete("./test/file-system-search/file.txt"); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), [ "./test/file-system-search/virtual-file.txt", "./test/file-system-search/another-file.txt", ]); t.is(fs.count, 1); fs.delete("./test/file-system-search/another-file.txt"); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), [ "./test/file-system-search/virtual-file.txt", ]); t.is(fs.count, 1); fs.delete("./test/file-system-search/virtual-file.txt"); t.deepEqual(await fs.search("key", "./test/file-system-search/*.txt"), []); t.is(fs.count, 1); }); ================================================ FILE: test/GetCollectionItemIndexTest.js ================================================ import test from "ava"; import getCollectionItemIndex from "../src/Filters/GetCollectionItemIndex.js"; test("getCollectionItemIndex", (t) => { let first = { inputPath: "hello.md", outputPath: "/hello/", }; let second = { inputPath: "hello2.md", outputPath: "/hello2/", }; let third = { inputPath: "hello3.md", outputPath: "/hello3/", }; let collections = [first, second, third]; t.deepEqual(getCollectionItemIndex(collections, first), 0); t.deepEqual(getCollectionItemIndex(collections, second), 1); t.deepEqual(getCollectionItemIndex(collections, third), 2); t.deepEqual( getCollectionItemIndex(collections, { inputPath: "unknown.md", outputPath: "/unknown/", }), undefined ); }); ================================================ FILE: test/GetCollectionItemTest.js ================================================ import test from "ava"; import getCollectionItem from "../src/Filters/GetCollectionItem.js"; test("getCollectionItem", (t) => { let first = { inputPath: "hello.md", outputPath: "/hello/", }; let second = { inputPath: "hello2.md", outputPath: "/hello2/", }; let third = { inputPath: "hello3.md", outputPath: "/hello3/", }; let collections = [first, second, third]; t.deepEqual(getCollectionItem(collections, first), first); t.deepEqual(getCollectionItem(collections, second), second); t.deepEqual(getCollectionItem(collections, third), third); t.deepEqual(getCollectionItem(collections, first, -1), undefined); t.deepEqual(getCollectionItem(collections, second, -1), first); t.deepEqual(getCollectionItem(collections, third, -1), second); t.deepEqual(getCollectionItem(collections, first, 1), second); t.deepEqual(getCollectionItem(collections, second, 1), third); t.deepEqual(getCollectionItem(collections, third, 1), undefined); }); ================================================ FILE: test/GlobRemapTest.js ================================================ import test from "ava"; import path from "node:path"; import GlobRemap from "../src/Util/GlobRemap.js"; import { normalizeSeparatorString } from "./Util/normalizeSeparators.js"; test("getParentDirPrefix", (t) => { t.is(GlobRemap.getParentDirPrefix(""), ""); t.is(GlobRemap.getParentDirPrefix("./test/"), ""); t.is(GlobRemap.getParentDirPrefix("../test/"), "../"); t.is(GlobRemap.getParentDirPrefix("../test/../"), "../"); t.is(GlobRemap.getParentDirPrefix("../../test/"), "../../"); }); test("getCwd", (t) => { t.is(GlobRemap.getCwd([]), ""); t.is(GlobRemap.getCwd(["test.njk"]), ""); t.is(GlobRemap.getCwd(["./test.njk"]), ""); t.is(GlobRemap.getCwd(["../test.njk"]), "../"); t.is(GlobRemap.getCwd(["../test.njk", "../../2.njk"]), "../../"); }); test("Constructor (control)", t => { let m = new GlobRemap([ '**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', '**/*.txt', // passthrough copy '**/*.png', '_includes/**', '_data/**', '.gitignore', '.eleventyignore', 'eleventy.config.js', ]) t.deepEqual(m.getInput(), [ '**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', '**/*.txt', // passthrough copy '**/*.png', '_includes/**', '_data/**', '.gitignore', '.eleventyignore', 'eleventy.config.js', ]) }); test("Constructor (control with ./)", t => { let m = new GlobRemap([ './**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', './**/*.txt', // passthrough copy './**/*.png', './_includes/**', './_data/**', './.gitignore', './.eleventyignore', './eleventy.config.js', ]) t.deepEqual(m.getInput(), [ './**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', './**/*.txt', // passthrough copy './**/*.png', './_includes/**', './_data/**', './.gitignore', './.eleventyignore', './eleventy.config.js', ]) }); test("Constructor (up one dir)", t => { let m = new GlobRemap([ '../**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', '../**/*.txt', // passthrough copy '../**/*.png', '../_includes/**', '../_data/**', './.gitignore', './.eleventyignore', '../.eleventyignore', './eleventy.config.js', ]) let parentDir = normalizeSeparatorString(path.resolve("./").split(path.sep).slice(-1).join(path.sep)); t.deepEqual(m.getInput(), [ '**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', '**/*.txt', // passthrough copy '**/*.png', '_includes/**', '_data/**', `${parentDir}/.gitignore`, `${parentDir}/.eleventyignore`, '.eleventyignore', `${parentDir}/eleventy.config.js`, ]) }); test("Constructor (up two dirs)", t => { let m = new GlobRemap([ '../../**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', '../**/*.txt', // passthrough copy '../**/*.png', '../_includes/**', '../_data/**', './.gitignore', './.eleventyignore', '../.eleventyignore', './eleventy.config.js', ]) let childDir = normalizeSeparatorString(path.resolve("./").split(path.sep).slice(-2).join(path.sep)); let parentDir = normalizeSeparatorString(path.resolve("./").split(path.sep).slice(-2, -1).join(path.sep)); t.deepEqual(m.getInput(), [ '**/*.{liquid,md,njk,html,11ty.js,11ty.cjs,11ty.mjs}', `${parentDir}/**/*.txt`, // passthrough copy `${parentDir}/**/*.png`, `${parentDir}/_includes/**`, `${parentDir}/_data/**`, `${childDir}/.gitignore`, `${childDir}/.eleventyignore`, `${parentDir}/.eleventyignore`, `${childDir}/eleventy.config.js`, ]) }); ================================================ FILE: test/GlobStripperTest.js ================================================ import test from "ava"; import { GlobStripper } from "../src/Util/GlobStripper.js"; test("Separate globs from directories", (t) => { t.deepEqual(GlobStripper.parse(""), { path: "." }); t.deepEqual(GlobStripper.parse("dir"), { path: "dir" }); t.deepEqual(GlobStripper.parse("./*"), { path: ".", glob: "*" }); t.deepEqual(GlobStripper.parse("*"), { path: ".", glob: "*" }); t.deepEqual(GlobStripper.parse("**"), { path: ".", glob: "**" }); t.deepEqual(GlobStripper.parse("**/*"), { path: ".", glob: "**/*" }); t.deepEqual(GlobStripper.parse("*/*"), { path: ".", glob: "*/*" }); t.deepEqual(GlobStripper.parse("**/**/*"), { path: ".", glob: "**/**/*" }); t.deepEqual(GlobStripper.parse(".dot/**"), { path: ".dot", glob: "**" }); t.deepEqual(GlobStripper.parse("dir/**"), { path: "dir", glob: "**" }); t.deepEqual(GlobStripper.parse("/dir/**"), { path: "/dir", glob: "**" }); t.deepEqual(GlobStripper.parse("dir/**/*"), { path: "dir", glob: "**/*" }); t.deepEqual(GlobStripper.parse("dir/**/*.{jpg,png}"), { path: "dir", glob: "**/*.{jpg,png}" }); }); test("Star not at start of filename", (t) => { t.deepEqual(GlobStripper.parse("a*.c*"), { path: ".", glob: "a*.c*" }); t.deepEqual(GlobStripper.parse("a/b-*/**/z.js"), { path: "a", glob: "b-*/**/z.js" }); }); test("Expected failures", (t) => { t.throws(() => GlobStripper.parse("?/?"), { message: "Could not automatically determine top-most folder from glob pattern: ?/?"}); }); test("Issue #3910", (t) => { t.deepEqual(GlobStripper.parse("./node_modules/artificial-chart/artificial-chart.css"), { path: "./node_modules/artificial-chart/artificial-chart.css" }); t.deepEqual(GlobStripper.parse("./node_modules/artificial-chart/artificial-chart.{css,js}"), { path: "./node_modules/artificial-chart", glob: "artificial-chart.{css,js}" }); t.deepEqual(GlobStripper.parse("./node_modules/artificial-chart/artificial-chart.(css|js)"), { path: "./node_modules/artificial-chart", glob: "artificial-chart.(css|js)" }); }); ================================================ FILE: test/GlobalDependencyMapTest.js ================================================ import test from "ava"; import GlobalDependencyMap from "../src/GlobalDependencyMap.js"; test("Test map", (t) => { let map = new GlobalDependencyMap(); map.addDependency("test.njk", ["_includes/include.njk"]); t.true(map.hasDependency("test.njk", "_includes/include.njk")); t.false(map.hasDependency("test.njk", "_includes/other.njk")); t.false(map.isFileRelevantTo("test.njk", null)); t.true(map.isFileRelevantTo("test.njk", "test.njk")); // if _includes/include.njk changes, we want to recompile test.njk t.true(map.isFileRelevantTo("test.njk", "_includes/include.njk")); t.false(map.isFileRelevantTo("_includes/include.njk", "test.njk")); }); test("Normalize nodes (remove leading dot slash)", (t) => { let map = new GlobalDependencyMap(); map.addDependency("./test.njk", ["./_includes/include.njk"]); t.true(map.hasDependency("./test.njk", "./_includes/include.njk")); t.false(map.hasDependency("./test.njk", "./_includes/other.njk")); t.true(map.isFileRelevantTo("./test.njk", "./_includes/include.njk")); t.false(map.isFileRelevantTo("./_includes/include.njk", "./test.njk")); }); test("Layouts", (t) => { let map = new GlobalDependencyMap(); map.addDependency("test.njk", ["_includes/include.njk"]); map.addLayoutsToMap({ "./_includes/layout.njk": ["./test.njk"], }); // if _layout/layout.njk changes, we want to write test.njk t.true(map.isFileRelevantTo("test.njk", "_includes/layout.njk")); t.false(map.isFileRelevantTo("_includes/layout.njk", "test.njk")); t.false(map.isFileRelevantTo("_includes/layout.njk", "_includes/include.njk")); t.false(map.isFileRelevantTo("_includes/include.njk", "_includes/layout.njk")); // if _layout/layout.njk changes, we don’t care about recompiling test.njk (ignore layouts) // though we do want to re-write test.njk so we want incremental to match t.false(map.isFileRelevantTo("test.njk", "_includes/layout.njk", false)); t.false(map.isFileRelevantTo("_includes/layout.njk", "test.njk", false)); t.false(map.isFileRelevantTo("_includes/layout.njk", "_includes/include.njk", false)); t.false(map.isFileRelevantTo("_includes/include.njk", "_includes/layout.njk", false)); }); test("Stringify/restore", (t) => { let origin = new GlobalDependencyMap(); origin.addDependency("test.njk", ["_includes/include.njk"]); t.is( origin.stringify(), `{"nodes":{"__collection:all":"__collection:all","__collection:[keys]":"__collection:[keys]","__collection:[userconfig]":"__collection:[userconfig]","__collection:[basic]":"__collection:[basic]","test.njk":"test.njk","_includes/include.njk":"_includes/include.njk"},"outgoingEdges":{"__collection:all":["__collection:[keys]"],"__collection:[keys]":["__collection:[userconfig]"],"__collection:[userconfig]":["__collection:[basic]"],"__collection:[basic]":[],"test.njk":["_includes/include.njk"],"_includes/include.njk":[]},"incomingEdges":{"__collection:all":[],"__collection:[keys]":["__collection:all"],"__collection:[userconfig]":["__collection:[keys]"],"__collection:[basic]":["__collection:[userconfig]"],"test.njk":[],"_includes/include.njk":["test.njk"]},"circular":true}` ); let map = new GlobalDependencyMap(); map.restore(origin.stringify()); t.true(map.hasDependency("test.njk", "_includes/include.njk")); t.false(map.hasDependency("test.njk", "_includes/other.njk")); t.false(map.isFileRelevantTo("test.njk", null)); t.true(map.isFileRelevantTo("test.njk", "test.njk")); // if _includes/include.njk changes, we want to recompile test.njk t.true(map.isFileRelevantTo("test.njk", "_includes/include.njk")); t.false(map.isFileRelevantTo("_includes/include.njk", "test.njk")); }); test("Collection API", (t) => { let map = new GlobalDependencyMap(); map.setCollectionApiNames(["articles"]); map.addNewNodeRelationships("test.njk", [], ["all"]) map.addNewNodeRelationships("feed.njk", ["articles"], ["all"]) t.deepEqual(map.getTemplateOrder(), [ "test.njk", "__collection:[keys]", "__collection:articles", "feed.njk", "__collection:all", ]); }); ================================================ FILE: test/HtmlBasePluginTest.js ================================================ import test from "ava"; import { default as HtmlBasePlugin, applyBaseToUrl } from "../src/Plugins/HtmlBasePlugin.js"; import Eleventy from "../src/Eleventy.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; function getContentFor(results, filename) { let content = results.filter((entry) => entry.outputPath.endsWith(filename))[0].content; return normalizeNewLines(content.trim()); } test("Using the filter directly", async (t) => { // url, base, pathprefix // default pathprefix t.is(applyBaseToUrl("/", "/"), "/"); t.is(applyBaseToUrl("/test/", "/"), "/test/"); t.is(applyBaseToUrl("subdir/", "/"), "subdir/"); t.is(applyBaseToUrl("../subdir/", "/"), "../subdir/"); t.is(applyBaseToUrl("./subdir/", "/"), "subdir/"); t.is(applyBaseToUrl("http://example.com/", "/"), "http://example.com/"); t.is(applyBaseToUrl("http://example.com/test/", "/"), "http://example.com/test/"); // relative url pathprefix is ignored t.is(applyBaseToUrl("/", "../"), "/"); t.is(applyBaseToUrl("/test/", "../"), "/test/"); t.is(applyBaseToUrl("subdir/", "../"), "subdir/"); t.is(applyBaseToUrl("../subdir/", "../"), "../subdir/"); t.is(applyBaseToUrl("./subdir/", "../"), "subdir/"); t.is(applyBaseToUrl("http://example.com/", "../"), "http://example.com/"); t.is(applyBaseToUrl("http://example.com/test/", "../"), "http://example.com/test/"); // with a pathprefix t.is(applyBaseToUrl("/", "/pathprefix/"), "/pathprefix/"); t.is(applyBaseToUrl("/test/", "/pathprefix/"), "/pathprefix/test/"); t.is(applyBaseToUrl("subdir/", "/pathprefix/"), "subdir/"); t.is(applyBaseToUrl("../subdir/", "/pathprefix/"), "../subdir/"); t.is(applyBaseToUrl("./subdir/", "/pathprefix/"), "subdir/"); t.is(applyBaseToUrl("#anchor", "/pathprefix/"), "#anchor"); t.is(applyBaseToUrl("/test/#anchor", "/pathprefix/"), "/pathprefix/test/#anchor"); t.is(applyBaseToUrl("/test/?param=value", "/pathprefix/"), "/pathprefix/test/?param=value"); t.is(applyBaseToUrl("http://url.com/", "/pathprefix/"), "http://url.com/"); t.is(applyBaseToUrl("http://url.com/test/", "/pathprefix/"), "http://url.com/test/"); // with a URL base t.is(applyBaseToUrl("/", "http://example.com/"), "http://example.com/"); t.is(applyBaseToUrl("/test/", "http://example.com/"), "http://example.com/test/"); t.is(applyBaseToUrl("subdir/", "http://example.com/"), "http://example.com/subdir/"); t.is(applyBaseToUrl("../subdir/", "http://example.com/"), "http://example.com/subdir/"); t.is(applyBaseToUrl("./subdir/", "http://example.com/"), "http://example.com/subdir/"); t.is(applyBaseToUrl("http://url.com/", "http://example.com/"), "http://url.com/"); t.is(applyBaseToUrl("http://url.com/test/", "http://example.com/"), "http://url.com/test/"); t.is(applyBaseToUrl("#anchor", "http://example.com/"), "http://example.com/#anchor"); t.is(applyBaseToUrl("/test/#anchor", "http://example.com/"), "http://example.com/test/#anchor"); t.is(applyBaseToUrl("/test/?param=value#anchor", "http://example.com/"), "http://example.com/test/?param=value#anchor"); // with a URL base with extra subdirectory t.is(applyBaseToUrl("/", "http://example.com/ignored/"), "http://example.com/"); t.is(applyBaseToUrl("/test/", "http://example.com/ignored/"), "http://example.com/test/"); t.is(applyBaseToUrl("subdir/", "http://example.com/deep/"), "http://example.com/deep/subdir/"); t.is(applyBaseToUrl("../subdir/", "http://example.com/deep/"), "http://example.com/subdir/"); t.is(applyBaseToUrl("./subdir/", "http://example.com/deep/"), "http://example.com/deep/subdir/"); t.is(applyBaseToUrl("http://url.com/", "http://example.com/ignored/"), "http://url.com/"); t.is( applyBaseToUrl("http://url.com/test/", "http://example.com/ignored/"), "http://url.com/test/" ); // with a URL base and root pathprefix t.is(applyBaseToUrl("/", "http://example.com/", { pathPrefix: "/" }), "http://example.com/"); t.is( applyBaseToUrl("/test/", "http://example.com/", { pathPrefix: "/" }), "http://example.com/test/" ); t.is( applyBaseToUrl("subdir/", "http://example.com/", { pathPrefix: "/" }), "http://example.com/subdir/" ); t.is( applyBaseToUrl("../subdir/", "http://example.com/", { pathPrefix: "/" }), "http://example.com/subdir/" ); t.is( applyBaseToUrl("./subdir/", "http://example.com/", { pathPrefix: "/" }), "http://example.com/subdir/" ); t.is( applyBaseToUrl("http://url.com/", "http://example.com/", { pathPrefix: "/", }), "http://url.com/" ); t.is( applyBaseToUrl("http://url.com/test/", "http://example.com/", { pathPrefix: "/", }), "http://url.com/test/" ); // with a base and pathprefix t.is( applyBaseToUrl("/", "http://example.com/", { pathPrefix: "/pathprefix/" }), "http://example.com/pathprefix/" ); t.is( applyBaseToUrl("/test/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://example.com/pathprefix/test/" ); t.is( applyBaseToUrl("subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://example.com/pathprefix/subdir/" ); t.is( applyBaseToUrl("../subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://example.com/pathprefix/subdir/" ); t.is( applyBaseToUrl("./subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://example.com/pathprefix/subdir/" ); t.is( applyBaseToUrl("http://url.com/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://url.com/" ); t.is( applyBaseToUrl("http://url.com/test/", "http://example.com/", { pathPrefix: "/pathprefix/", }), "http://url.com/test/" ); // with a base and pathprefix and page url (for relative path urls) t.is( applyBaseToUrl("/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/" ); t.is( applyBaseToUrl("/test/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/test/" ); t.is( applyBaseToUrl("subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/deep/subdir/" ); t.is( applyBaseToUrl("../subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/subdir/" ); t.is( applyBaseToUrl("./subdir/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/deep/subdir/" ); t.is( applyBaseToUrl("http://url.com/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://url.com/" ); t.is( applyBaseToUrl("http://url.com/test/", "http://example.com/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://url.com/test/" ); // with a base (with extra subdir) and pathprefix and page url (for relative path urls) // Note: Extra subdir is ignored when pageUrl is in play t.is( applyBaseToUrl("/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/" ); t.is( applyBaseToUrl("/test/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/test/" ); t.is( applyBaseToUrl("subdir/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/deep/subdir/" ); t.is( applyBaseToUrl("../subdir/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/subdir/" ); t.is( applyBaseToUrl("./subdir/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://example.com/pathprefix/deep/subdir/" ); t.is( applyBaseToUrl("http://url.com/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://url.com/" ); t.is( applyBaseToUrl("http://url.com/test/", "http://example.com/ignored/", { pathPrefix: "/pathprefix/", pageUrl: "/deep/", }), "http://url.com/test/" ); }); test("Using the HTML base plugin (default values)", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Using the HTML base plugin with pathPrefix: /test/", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Using the HTML base plugin with pathPrefix: /test/ and base: http://example.com/", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin, { baseHref: "http://example.com/", }); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Using the HTML base plugin strips extra path in full URL base (default pathPrefix)", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin, { baseHref: "http://example.com/hello/", // extra path will be stripped }); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Using the HTML base plugin strips extra path in full URL base (pathPrefix: /test/)", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin, { baseHref: "http://example.com/hello/", // extra path will be stripped }); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Opt out of the transform with falsy extensions list", async (t) => { let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin, { extensions: false, }); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test Test ` ); }); test("Base plugin with permalink: false, #2602", async (t) => { let elev = new Eleventy("./test/stubs-2602/", "./test/stubs-2602/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test ` ); }); test("Using the HTML base plugin with pathPrefix: /test/ and transformed attributes are *not* case sensitive", async (t) => { let elev = new Eleventy("./test/stubs-base-case-sens/", "./test/stubs-base-case-sens/_site", { pathPrefix: "/test/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addPlugin(HtmlBasePlugin); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` Home Test Test ` ); }); test("HTML base plugin only adds once (unique)", async (t) => { t.plan(2); let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { configPath: false, config: function (eleventyConfig) { // Runs before defaultConfig.js t.is(eleventyConfig.plugins.length, 0); eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPlugin(HtmlBasePlugin); t.is(eleventyConfig.plugins.length, 1); }, }); await elev.init(); }); test("HTML base plugin can resolve by name", async (t) => { t.plan(2); let elev = new Eleventy("./test/stubs-base/", "./test/stubs-base/_site", { configPath: false, config: async function (eleventyConfig) { // Runs before defaultConfig.js t.is(eleventyConfig.plugins.length, 0); let plugin = await eleventyConfig.resolvePlugin("@11ty/eleventy/html-base-plugin"); eleventyConfig.addPlugin(plugin); // does not add duplicate eleventyConfig.addPlugin(plugin); // does not add duplicate even with a different reference eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPlugin(HtmlBasePlugin); t.is(eleventyConfig.plugins.length, 1); }, }); await elev.init(); }); test("Using recognizeNoValueAttribute for boolean attributes without quotes #2766", async (t) => { let elev = new Eleventy({ input: "./test/stubs-virtual/", pathPrefix: "/prefixed/", configPath: false, config: function (eleventyConfig) { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addTemplate("index.njk", `--- permalink: /deep/ --- `) eleventyConfig.addPlugin(HtmlBasePlugin); }, }); await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); let results = await elev.toJSON(); t.is( getContentFor(results, "/deep/index.html"), ` ` ); }); ================================================ FILE: test/HtmlRelativeCopyTest.js ================================================ import test from "ava"; import fs from "node:fs"; import { TemplatePath } from "@11ty/eleventy-utils"; import { globSync } from "tinyglobby"; import { TransformPlugin as InputPathToUrlTransformPlugin } from "../src/Plugins/InputPathToUrl.js"; import { default as HtmlBasePlugin } from "../src/Plugins/HtmlBasePlugin.js"; import Eleventy from "../src/Eleventy.js"; import { deleteDirectory } from "./_testHelpers.js"; test.after.always("Directory cleanup", () => { let dirs = globSync("./test/stubs-autocopy/_site*", { onlyDirectories: true, expandDirectories: false, }); for(let dir of dirs) { deleteDirectory(dir); } }) test("Basic usage", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site-basica", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative" }) eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: { "/test/possum.png": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png") } }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 1); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site-basica/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site-basica/test/possum.png"), } }); t.is(fs.existsSync("test/stubs-autocopy/_site-basica/test/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site-basica/test/index.html"), true); }); test("More complex image path (parent dir)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site-basicb", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative" }) eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: { "/stubs-img-transform/possum.png": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-img-transform/possum.png") } }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 1); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site-basicb/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 1, map: { // test/stubs-autocopy/test.njk => "../stubs-img-transform/possum.png" [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-img-transform/possum.png")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site-basicb/stubs-img-transform/possum.png"), } }); t.is(fs.existsSync("test/stubs-autocopy/_site-basicb/stubs-img-transform/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site-basicb/test/index.html"), true); }); test("No matches", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site2", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.jpeg", { mode: "html-relative" }) eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 0); t.is(templates.length, 1); t.is(fs.existsSync("test/stubs-autocopy/_site2/test/lol.lol"), false); t.is(fs.existsSync("test/stubs-autocopy/_site2/test/index.html"), true); }); test("Match but does not exist (throws error)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site3", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); await t.throwsAsync(async () => { await elev.write(); }, { message: `Having trouble writing to "./test/stubs-autocopy/_site3/test/index.html" from "./test/stubs-autocopy/test.njk"` }); t.is(fs.existsSync("test/stubs-autocopy/_site3/test/index.html"), false); }); test("Match but does not exist (no error, using `failOnError: false`)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site4", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative", failOnError: false, }) eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 0); t.is(templates.length, 1); t.is(fs.existsSync("test/stubs-autocopy/_site4/test/missing.png"), false); t.is(fs.existsSync("test/stubs-autocopy/_site4/test/index.html"), true); }); test("Copying dotfiles are not allowed", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site5", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // WARNING: don’t do this eleventyConfig.addPassthroughCopy("**/*", { mode: "html-relative", copyOptions: { // debug: true, } }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 1); t.is(copy[0].count, 0); t.is(templates.length, 1); t.is(fs.existsSync("test/stubs-autocopy/_site5/.gitkeep"), false); t.is(fs.existsSync("test/stubs-autocopy/_site5/test/.gitkeep"), false); t.is(fs.existsSync("test/stubs-autocopy/_site5/test/index.html"), true); }); test("Using with InputPathToUrl plugin", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site6", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // order of addPlugin shouldn’t matter here eleventyConfig.addPassthroughCopy("**/*.{html,njk}", { mode: "html-relative" }); eleventyConfig.addPlugin(InputPathToUrlTransformPlugin); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test1.njk", `Test 1`) eleventyConfig.addTemplate("test2.njk", `Test 2`) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 0); t.is(templates.length, 2); t.is(templates.filter(entry => entry.url.endsWith("/test2/"))[0].content, `Test 2`); t.is(fs.existsSync("test/stubs-autocopy/_site6/test2/test1.njk"), false); t.is(fs.existsSync("test/stubs-autocopy/_site6/test2/index.html"), true); }); test("Using with InputPathToUrl plugin (reverse addPlugin order)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site7", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // order of addPlugin shouldn’t matter here eleventyConfig.addPlugin(InputPathToUrlTransformPlugin); eleventyConfig.addPassthroughCopy("**/*.{html,njk}", { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test1.njk", `Test 1`) eleventyConfig.addTemplate("test2.njk", `Test 2`) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 0); t.is(templates.length, 2); t.is(templates.filter(entry => entry.url.endsWith("/test2/"))[0].content, `Test 2`); t.is(fs.existsSync("test/stubs-autocopy/_site7/test2/test1.njk"), false); t.is(fs.existsSync("test/stubs-autocopy/_site7/test2/index.html"), true); }); test("Use with HtmlBasePlugin usage", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site8a", { configPath: false, pathPrefix: "yolo", config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: { "/test/possum.png": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png") } }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 1); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site8a/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site8a/test/possum.png"), } }); t.is(fs.existsSync("test/stubs-autocopy/_site8a/test/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site8a/test/index.html"), true); }); test("Using with InputPathToUrl plugin and HtmlBasePlugin", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site8b", { configPath: false, pathPrefix: "yolo", config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // order of addPlugin shouldn’t matter here eleventyConfig.addPassthroughCopy("**/*.{html,njk}", { mode: "html-relative" }); eleventyConfig.addPlugin(InputPathToUrlTransformPlugin); eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test1.njk", `Test 1`) eleventyConfig.addTemplate("test2.njk", `Test 2`) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 0); t.is(templates.length, 2); t.is(templates.filter(entry => entry.url.endsWith("/test2/"))[0].content, `Test 2`); t.is(fs.existsSync("test/stubs-autocopy/_site8b/test2/test1.njk"), false); t.is(fs.existsSync("test/stubs-autocopy/_site8b/test2/index.html"), true); }); test("Multiple addPlugin calls (use both globs)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site9", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.jpg", { mode: "html-relative" }); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: { "/test/possum.jpg": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.jpg"), "/test/possum.png": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png"), } }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 2); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site9/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site9/test/possum.png"), } }); t.deepEqual(copy[1], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.jpg")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site9/test/possum.jpg"), } }); t.is(fs.existsSync("test/stubs-autocopy/_site9/test/possum.jpg"), true); t.is(fs.existsSync("test/stubs-autocopy/_site9/test/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site9/test/index.html"), true); }); test("Array of globs", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site10", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy(["**/*.jpg", "**/*.png"], { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: { "/test/possum.jpg": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.jpg"), "/test/possum.png": TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png"), } }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 2); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site10/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.png")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site10/test/possum.png"), } }); t.deepEqual(copy[1], { count: 1, map: { [TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/possum.jpg")]: TemplatePath.normalizeOperatingSystemFilePath("test/stubs-autocopy/_site10/test/possum.jpg"), } }); t.is(fs.existsSync("test/stubs-autocopy/_site10/test/possum.jpg"), true); t.is(fs.existsSync("test/stubs-autocopy/_site10/test/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site10/test/index.html"), true); }); test("overwrite: false", async (t) => { fs.mkdirSync("./test/stubs-autocopy/_site11/test/", { recursive: true }) fs.copyFileSync("./test/stubs-autocopy/possum.png", "./test/stubs-autocopy/_site11/test/possum.png"); let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site11", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); eleventyConfig.addPassthroughCopy("**/*.png", { mode: "html-relative", copyOptions: { overwrite: false, } }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); let [copy, templates] = await elev.write(); t.is(copy.length, 1); t.is(templates.length, 1); t.deepEqual(templates[0], { inputPath: './test/stubs-autocopy/test.njk', outputPath: './test/stubs-autocopy/_site11/test/index.html', url: '/test/', content: '', rawInput: '' }); t.deepEqual(copy[0], { count: 0, map: {} }); t.is(fs.existsSync("test/stubs-autocopy/_site11/test/possum.png"), true); t.is(fs.existsSync("test/stubs-autocopy/_site11/test/index.html"), true); }); test("Input -> output remapping not yet supported (throws error)", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site12", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // not yet supported eleventyConfig.addPassthroughCopy({"**/*.png": "yo"}, { mode: "html-relative" }); eleventyConfig.on("eleventy.passthrough", copyMap => { t.deepEqual(copyMap, { map: {} }) }); eleventyConfig.addTemplate("test.njk", ``) }, }); elev.disableLogger(); await t.throwsAsync(async () => { await elev.write(); }, { message: `mode: 'html-relative' does not yet support passthrough copy objects (input -> output mapping). Use a string glob or an Array of string globs.` }); t.is(fs.existsSync("test/stubs-autocopy/_site12/test/index.html"), false); }); test("Invalid copy mode throws error", async (t) => { let elev = new Eleventy("./test/stubs-autocopy/", "./test/stubs-autocopy/_site13", { configPath: false, config: function (eleventyConfig) { // Node 24: workaround for re-using input directory (and not ignoring all output directories by default) eleventyConfig.ignores.add("./test/stubs-autocopy/_site*/**"); // not yet supported eleventyConfig.addPassthroughCopy({"**/*.png": "yo"}, { mode: "throw-an-error" }); }, }); elev.disableLogger(); await t.throwsAsync(async () => { await elev.write(); }, { message: `Invalid \`mode\` option for \`addPassthroughCopy\`. Received: 'throw-an-error'` }); t.is(fs.existsSync("test/stubs-autocopy/_site13/test/index.html"), false); }); ================================================ FILE: test/I18nPluginTest.js ================================================ import test from "ava"; import { Comparator, LangUtils, default as I18nPlugin } from "../src/Plugins/I18nPlugin.js"; import Eleventy from "../src/Eleventy.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; test("Comparator.isLangCode", (t) => { t.is(Comparator.isLangCode(null), false); t.is(Comparator.isLangCode(undefined), false); t.is(Comparator.isLangCode("en"), true); t.is(Comparator.isLangCode("en-us"), true); t.is(Comparator.isLangCode("dee"), false); t.is(Comparator.isLangCode("en_us"), false); t.is(Comparator.isLangCode("d"), false); t.is(Comparator.isLangCode("deed"), false); t.is(Comparator.isLangCode("deede"), false); t.is(Comparator.isLangCode("deedee"), false); }); test("LangUtils.swapLanguageCode", (t) => { t.is(LangUtils.swapLanguageCode("/"), "/"); // skip t.is(LangUtils.swapLanguageCode("/", "en"), "/"); // skip t.is(LangUtils.swapLanguageCode("/es/", "en"), "/en/"); t.is(LangUtils.swapLanguageCode("/es/", "not"), "/es/"); // skip t.is(LangUtils.swapLanguageCode("/not-a-lang/", "en"), "/not-a-lang/"); // skip t.is(LangUtils.swapLanguageCode("/es/es/es/", "en"), "/en/es/es/"); // first only }); test("contentMap Event from Eleventy", async (t) => { t.plan(4); let elev = new Eleventy("./test/stubs-i18n/", "./test/stubs-i18n/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(I18nPlugin, { defaultLanguage: "en", errorMode: "allow-fallback", }); eleventyConfig.on("eleventy.contentMap", (maps) => { t.truthy(maps); // if future maps are added, they should be tested here t.is(Object.keys(maps).length, 2); t.deepEqual(maps.urlToInputPath, { "/en/": { inputPath: "./test/stubs-i18n/en/index.liquid", groupNumber: 0 }, "/en-us/": { inputPath: "./test/stubs-i18n/en-us/index.11ty.cjs", groupNumber: 0, }, "/es/": { inputPath: "./test/stubs-i18n/es/index.njk", groupNumber: 0 }, "/non-lang-file/": { inputPath: "./test/stubs-i18n/non-lang-file.njk", groupNumber: 0 }, }); t.deepEqual(maps.inputPathToUrl, { "./test/stubs-i18n/en/index.liquid": ["/en/"], "./test/stubs-i18n/en-us/index.11ty.cjs": ["/en-us/"], "./test/stubs-i18n/es/index.njk": ["/es/"], "./test/stubs-i18n/non-lang-file.njk": ["/non-lang-file/"], }); }); }, }); await elev.toJSON(); }); function getContentFor(results, filename) { let content = results.filter((entry) => entry.inputPath.endsWith(filename))[0].content; return normalizeNewLines(content.trim()); } test("errorMode default (strict)", async (t) => { let elev = new Eleventy("./test/stubs-i18n/", "./test/stubs-i18n/_site", { quietMode: true, config: function (eleventyConfig) { eleventyConfig.addPlugin(I18nPlugin, { _test: "this is from errorMode default (strict)", defaultLanguage: "en", // errorMode: "allow-fallback" }); }, }); // TODO get rid of these? await elev.initializeConfig(); elev.setIsVerbose(false); elev.disableLogger(); await t.throwsAsync(async () => { await elev.toJSON(); }); }); test("locale_url and locale_links Filters", async (t) => { let elev = new Eleventy("./test/stubs-i18n/", "./test/stubs-i18n/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(I18nPlugin, { _test: "this is from locale_url and locale_links Filters", defaultLanguage: "en", errorMode: "allow-fallback", }); }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/non-lang-file.njk"), `/en/ /en-us/ /non-lang-file/ [] [] en` ); t.is( getContentFor(results, "/es/index.njk"), `/es/ /es/ /es/ /en-us/ /non-lang-file/ [{"url":"/en/","lang":"en","label":"English"},{"url":"/en-us/","lang":"en-us","label":"English"}] [{"url":"/en/","lang":"en","label":"English"},{"url":"/en-us/","lang":"en-us","label":"English"}] es` ); t.is( getContentFor(results, "/en/index.liquid"), `/en/ /en/ /en/ /en-us/ /non-lang-file/ [{"url":"/en-us/","lang":"en-us","label":"English"},{"url":"/es/","lang":"es","label":"Español"}] [{"url":"/en-us/","lang":"en-us","label":"English"},{"url":"/es/","lang":"es","label":"Español"}] en` ); t.is( getContentFor(results, "/en-us/index.11ty.cjs"), `/en-us/ /en-us/ /en-us/ /es/ /non-lang-file/ [{"url":"/en/","lang":"en","label":"English"},{"url":"/es/","lang":"es","label":"Español"}] [{"url":"/en/","lang":"en","label":"English"},{"url":"/es/","lang":"es","label":"Español"}] en-us` ); }); ================================================ FILE: test/IdAttributePluginTest.js ================================================ import test from "ava"; import { IdAttributePlugin } from "../src/Plugins/IdAttributePlugin.js"; import Eleventy from "../src/Eleventy.js"; test("Using the IdAttribute plugin #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.njk", `

This is a heading

This is another heading

This is another heading

This is another heading

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, `

This is a heading

This is another heading

This is another heading

This is another heading

`); }); test("Using the IdAttribute plugin, ignore attribute #3356", async (t) => { let elev = new Eleventy({ input: "./test/stubs-3356/", // configPath: false, config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.njk", `

This is a heading

This is another heading

This is another heading

This is another heading

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, `

This is a heading

This is another heading

This is another heading

This is another heading

`); }); test("Using the IdAttribute plugin with escaped quoted text", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.md", `# This is a \`"heading"\``, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

This is a "heading"

`); }); test("Issue #3424, id attribute conflicts (id attribute supplied first)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

`); }); test("Issue #3424, id attribute conflicts (id attribute supplied last)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

`); }); test("Issue #3424, id attribute conflicts (hard coded id conflicts)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

Testing

`); }); test("Issue #3424, id attribute conflicts (three deep, hard coded id conflicts)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

Testing

Testing

`); }); test("Issue #3424, id attribute conflicts (four deep, hard coded id conflicts)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

Testing

Testing

Testing

`); }); test("Issue #3424, id attribute conflicts (five deep, hard coded id conflicts)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

Testing

Testing

`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

Testing

Testing

Testing

Testing

`); }); test("Issue #3424, id attribute conflicts (two hard coded id conflicts)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin); eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); }, }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON()); t.is(e.originalError.originalError.toString(), `Error: You have more than one HTML \`id\` attribute using the same value (id="testing") in your template (./test/stubs-virtual/test.html). You can disable this error in the IdAttribute plugin with the \`checkDuplicates: false\` option.`); }); test("Issue #3424, filter callback skips", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(IdAttributePlugin, { filter: function({ page }) { if(page.inputPath.endsWith("test-skipped.html")) { return false; } return true; } }); eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); eleventyConfig.addTemplate("test-skipped.html", `

Testing

Testing

`, {}); }, }); elev.disableLogger(); let results = await elev.toJSON(); t.is(results[0].content.trim(), `

Testing

Testing

`); t.is(results[1].content.trim(), `

Testing

Testing

`); }); ================================================ FILE: test/ImportJsonSyncTest.js ================================================ import test from "ava"; import { TemplatePath } from "@11ty/eleventy-utils"; import { importJsonSync, findFilePathInParentDirs } from "../src/Util/ImportJsonSync.js"; test("Import a JSON", t => { t.deepEqual(Object.keys(importJsonSync("../../package.json")).sort().slice(-2, -1).pop(), "version"); }); test("getWorkingProjectPackageJson() traverse parent dirs", t => { let path = findFilePathInParentDirs(TemplatePath.absolutePath("test"), "package.json"); let json = importJsonSync(path); t.deepEqual(Object.keys(json).sort().slice(-2, -1).pop(), "version"); }); ================================================ FILE: test/InputPathToUrlPluginTest.js ================================================ import test from "ava"; import { TransformPlugin } from "../src/Plugins/InputPathToUrl.js"; import { default as HtmlBasePlugin } from "../src/Plugins/HtmlBasePlugin.js"; import Eleventy from "../src/Eleventy.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; const OUTPUT_HTML_STD = ` Home Test Anchor Anchor Anchor Search Search `; const OUTPUT_HTML_BASE = ` Home Test Anchor Anchor Anchor Search Search `; function getContentFor(results, filename) { let content = results.filter((entry) => entry.outputPath.endsWith(filename))[0].content; return normalizeNewLines(content.trim()); } test("Using the transform (and the filter too)", async (t) => { let elev = new Eleventy("./test/stubs-pathtourl/", "./test/stubs-pathtourl/_site", { configPath: false, config: function (eleventyConfig) { // FilterPlugin is available in the default config. eleventyConfig.addPlugin(TransformPlugin); }, }); let results = await elev.toJSON(); // filter is already available in the default config. t.is( getContentFor(results, "/filter/index.html"), OUTPUT_HTML_STD ); t.is( getContentFor(results, "/transform/index.html"), OUTPUT_HTML_STD ); }); test("Using the filter", async (t) => { let elev = new Eleventy("./test/stubs-pathtourl/", "./test/stubs-pathtourl/_site", { // FilterPlugin is available in the default config. configPath: false, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/filter/index.html"), OUTPUT_HTML_STD ); t.not( getContentFor(results, "/transform/index.html"), OUTPUT_HTML_STD ); }); test("Using the transform and the base plugin", async (t) => { let elev = new Eleventy("./test/stubs-pathtourl/", "./test/stubs-pathtourl/_site", { configPath: false, pathPrefix: "/gh-pages/", config: function (eleventyConfig) { eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addPlugin(HtmlBasePlugin); }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/filter/index.html"), OUTPUT_HTML_BASE ); t.is( getContentFor(results, "/transform/index.html"), OUTPUT_HTML_BASE ); }); test("Using the transform and the base plugin, reverse order", async (t) => { let elev = new Eleventy("./test/stubs-pathtourl/", "./test/stubs-pathtourl/_site", { configPath: false, pathPrefix: "/gh-pages/", config: function (eleventyConfig) { eleventyConfig.addPlugin(HtmlBasePlugin); eleventyConfig.addPlugin(TransformPlugin); }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/filter/index.html"), OUTPUT_HTML_BASE ); t.is( getContentFor(results, "/transform/index.html"), OUTPUT_HTML_BASE ); }); test("Issue #3417 Using the transform with relative path (dot slash)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { // FilterPlugin is available in the default config. eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addTemplate("source/test.njk", `Target`) eleventyConfig.addTemplate("source/target.njk", "lol") }, }); let results = await elev.toJSON(); // filter is already available in the default config. t.is( getContentFor(results, "/source/test/index.html"), `Target` ); }); test("Issue #3417 Using the transform with relative path (no dot slash)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { // FilterPlugin is available in the default config. eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addTemplate("source/test.njk", `Target`) eleventyConfig.addTemplate("source/target.njk", "lol") }, }); let results = await elev.toJSON(); // filter is already available in the default config. t.is( getContentFor(results, "/source/test/index.html"), `Target` ); }); test("Issue #3581 #build-cost-🧰", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addTemplate("source/test.njk", `Target`) }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/source/test/index.html"), `Target` ); }); test("Issue #3583 Markdown diacritics (no plugin)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addTemplate("test.md", `[Target]()`) }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/test/index.html"), `

Target

` ); }); test("Issue #3583 Diacritics", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addTemplate("test.md", `[Target](/hypothèse.md)`) eleventyConfig.addTemplate("hypothèse.md", "lol") }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/test/index.html"), `

Target

` ); }); test("Issue #3583 Diacritics Markdown raw", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addPlugin(TransformPlugin); eleventyConfig.addTemplate("test.md", `[Target]()`) eleventyConfig.addTemplate("hypothèse.md", "lol") }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/test/index.html"), `

Target

` ); }); test("Issue #3583 #3559 Markdown link with spaces (no plugin)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addTemplate("test.md", `[Target]()`) }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/test/index.html"), `

Target

` ); }); test("Issue #3583 #3559 Markdown spaces", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { configPath: false, config: function (eleventyConfig) { eleventyConfig.addPlugin(TransformPlugin); // eleventyConfig.addFilter("encode_uri_component", encodeURIComponent); eleventyConfig.addTemplate("test.md", `[Target]()`) eleventyConfig.addTemplate("target 1.md", "lol", { permalink: "/{{ page.fileSlug | slugify }}/" }) }, }); let results = await elev.toJSON(); t.is( getContentFor(results, "/test/index.html"), `

Target

` ); }); ================================================ FILE: test/Issue3467Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("Empty collections api #3467 (return undefined)", async (t) => { let elev = new Eleventy("./test/stubs-virtual", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`); eleventyConfig.addCollection("brokenCollection", function(collection) { // returns nothing }); }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); }); test("Empty collections api #3467 (return false)", async (t) => { let elev = new Eleventy("./test/stubs-virtual", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`); eleventyConfig.addCollection("brokenCollection", function(collection) { return false; }); }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); }); test("Empty collections api #3467 (return empty string)", async (t) => { let elev = new Eleventy("./test/stubs-virtual", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addTemplate("virtual.md", `# Hello`); eleventyConfig.addCollection("brokenCollection", function(collection) { return ""; }); }, }); let results = await elev.toJSON(); t.deepEqual(results.length, 1); t.deepEqual(results[0].content.trim(), `

Hello

`); t.deepEqual(results[0].rawInput, `# Hello`); }); ================================================ FILE: test/Issue3788Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3788 Nunjucks shortcodes args", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `{% test %}:{% test "" %}`); eleventyConfig.addShortcode("test", (args) => { return JSON.stringify(args); }) } }); let [result] = await elev.toJSON(); t.is(result.content, `undefined:""`); }); ================================================ FILE: test/Issue3797Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3797 Virtual templates with empty includes", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.setIncludesDirectory(""); eleventyConfig.setLayoutsDirectory("_layouts"); eleventyConfig.addTemplate("post1.md", "# Post1", { layout: "layout.html" }); eleventyConfig.addTemplate("_layouts/layout.html", "{{ content }}"); } }); let [result] = await elev.toJSON(); t.truthy(result); }); ================================================ FILE: test/Issue3808Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3808 addCollection in eleventy.before", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("post1.md", "# Post1"); eleventyConfig.addTemplate("post2.md", "# Post2"); eleventyConfig.addTemplate("index.njk", "{{ collections.posts.length }}"); eleventyConfig.on("eleventy.before", async () => { // eleventyConfig.on("eleventy.beforeConfig", async (eleventyConfig) => { eleventyConfig.addCollection("posts", async collectionApi => { return collectionApi.getFilteredByGlob("**/post*.md"); }); }) } }); let result = await elev.toJSON(); t.is(result.filter((entry) => entry.url === "/")[0]?.content.trim(), "2") }); // /* broken */ // export default function(eleventyConfig) { // eleventyConfig.on("eleventy.before", async () => { // eleventyConfig.addCollection("posts", collectionApi => { // return collectionApi.getFilteredByGlob("**/post*.md"); // }); // }) // } // /* works */ // export default function(eleventyConfig) { // eleventyConfig.on("eleventy.beforeConfig", async (eleventyConfig) => { // eleventyConfig.addCollection("posts", collectionApi => { // return collectionApi.getFilteredByGlob("**/post*.md"); // }); // }) // } // /* works */ // export default async function(eleventyConfig) { // eleventyConfig.addCollection("posts", collectionApi => { // return collectionApi.getFilteredByGlob("**/post*.md"); // }); // } // /* works */ // export default function(eleventyConfig) { // eleventyConfig.addCollection("posts", async collectionApi => { // return collectionApi.getFilteredByGlob("**/post*.md"); // }); // } ================================================ FILE: test/Issue3809Test.js ================================================ import test from "ava"; import { spawnAsync } from "../src/Util/spawn.js"; test("#3809 parent directory for content, with global data files", async (t) => { let result = await spawnAsync( "node", // Formats https://www.git-scm.com/docs/git-log#_pretty_formats // %at author date, UNIX timestamp ["../../../../cmd.cjs", "--to=json"], { cwd: "test/_issues/3809/.app/" } ); let json = JSON.parse(result); t.is(json.length, 1); t.is(json[0]?.content.trim(), "My Application"); }); ================================================ FILE: test/Issue3816Test.js ================================================ import markdownIt from "markdown-it"; import markdownItAbbr from "markdown-it-abbr"; import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3816 amendLibrary and setLibrary together", async (t) => { t.plan(1); let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("index.md", "# Heading"); eleventyConfig.setLibrary("md", markdownIt()); eleventyConfig.amendLibrary("md", (mdLib) => { // this will only run once, t.plan is important! let before = mdLib.core.ruler.getRules("").length; mdLib.use(markdownItAbbr); let after = mdLib.core.ruler.getRules("").length; t.is(after, before + 1); }); } }); await elev.toJSON(); await elev.restart(); await elev.toJSON(); await elev.restart(); await elev.toJSON(); await elev.restart(); }); ================================================ FILE: test/Issue3818Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; import WebCPlugin from "@11ty/eleventy-plugin-webc"; test("#3818 WebC Permalink", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addPlugin(WebCPlugin); eleventyConfig.addTemplate("index.webc", `--- eleventyComputed: permalink: "page//" ---`); } }); let [result] = await elev.toJSON(); t.is(result.url, "/page/1/"); }); test("#3818 WebC Permalink Pagination JavaScript function", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addPlugin(WebCPlugin); eleventyConfig.addTemplate("index.webc", `---js const pagination = { data: "posts", size: 2, }; function permalink(data) { return \`page/\${data.pagination.pageNumber + 1}/\`; } --- `, { posts: [ "first", "second", "third", "fourth", ] }); } }); let [page1, page2] = await elev.toJSON(); t.is(page1.url, "/page/1/"); t.is(page1.content, ` `) t.is(page2.url, "/page/2/"); t.is(page2.content, ` `) }); test("#3818 WebC Permalink Pagination, eleventyComputed.permalink String", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addPlugin(WebCPlugin); eleventyConfig.addTemplate("index.webc", `--- pagination: data: posts size: 2 eleventyComputed: permalink: "\`page/$\{pagination.pageNumber + 1}/\`" --- `, { posts: [ "first", "second", "third", "fourth", ] }); } }); let [page1, page2] = await elev.toJSON(); t.is(page1.url, "/page/1/"); t.is(page1.content, ` `) t.is(page2.url, "/page/2/"); t.is(page2.content, ` `) }); test("#3818 WebC Permalink Pagination, permalink String", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addPlugin(WebCPlugin); eleventyConfig.addTemplate("index.webc", `--- pagination: data: posts size: 2 permalink: "\`page/$\{pagination.pageNumber + 1}/\`" --- `, { posts: [ "first", "second", "third", "fourth", ] }); } }); let [page1, page2] = await elev.toJSON(); t.is(page1.url, "/page/1/"); t.is(page1.content, ` `) t.is(page2.url, "/page/2/"); t.is(page2.content, ` `) }); ================================================ FILE: test/Issue3823Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3823 addCollection -> pagination over `collections`", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("post1.md", "# Post1"); eleventyConfig.addTemplate("post2.md", "# Post2"); eleventyConfig.addTemplate("index.njk", `--- pagination: data: collections size: 1 alias: tag filter: - all addAllPagesToCollections: true --- {{ tag }}`); eleventyConfig.addCollection("posts", async collectionApi => { return collectionApi.getFilteredByGlob("**/post*.md"); }); } }); let results = await elev.toJSON(); results.sort(); t.is(results.length, 3); t.is(results.filter((entry) => entry.url === "/")[0]?.content.trim(), "posts") }); ================================================ FILE: test/Issue3825Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3825 #3834 addCollection consumes tag from pagination template", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("post1.md", "# Post1"); eleventyConfig.addTemplate("post2.md", "# Post2"); eleventyConfig.addCollection("posts", async collectionApi => { return collectionApi.getFilteredByGlob("**/post*.md"); }); eleventyConfig.addCollection("myCollection", collectionApi => { // populated by child.njk return collectionApi.getFilteredByTag("childTag"); }) eleventyConfig.addTemplate("child.njk", `--- pagination: data: collections.posts size: 1 alias: tag filter: - all addAllPagesToCollections: true tags: childTag --- {{ tag }}`); eleventyConfig.addTemplate("index.njk", `{{ collections.myCollection.length }}`, { // eleventyImport: { // collections: ["myCollection"] // } }); } }); let results = await elev.toJSON(); t.is(results.length, 5); t.is(results.filter((entry) => entry.url === "/")[0]?.content.trim(), "2") }); test("#3825 addCollection consumes tag from pagination template", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("post1.md", "# Post1"); eleventyConfig.addTemplate("post2.md", "# Post2"); eleventyConfig.addCollection("homepageLinks", function(collectionApi) { // glob consumes pagination over another userconfig collection return collectionApi.getFilteredByGlob(["**/principles.njk"]); }); eleventyConfig.addCollection("getAllPrinciplesOrderedByTitle", function(collectionApi) { return collectionApi.getFilteredByGlob("**/post*.md"); }); eleventyConfig.addTemplate("principles.njk", `--- pagination: data: collections.getAllPrinciplesOrderedByTitle size: 1 alias: tag filter: - all addAllPagesToCollections: true --- {{ tag }}`); eleventyConfig.addTemplate("index.njk", `{{ collections.homepageLinks.length }}`, { // eleventyImport: { // collections: ["homepageLinks"] // } }); } }); let results = await elev.toJSON(); t.is(results.length, 5); t.is(results.filter((entry) => entry.url === "/")[0]?.content.trim(), "2") }); test("Side-issue #3825 #3834 tried to Reflect.has on a string in pagination", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("post1.md", "# Post1"); eleventyConfig.addTemplate("post2.md", "# Post2"); eleventyConfig.addCollection("posts", async collectionApi => { return collectionApi.getFilteredByGlob("**/post*.md"); }); eleventyConfig.addCollection("myCollection", collectionApi => { // populated by child.njk return collectionApi.getFilteredByTag("someArbitraryTag"); }) eleventyConfig.addTemplate("child.njk", `--- pagination: data: collections.posts size: 1 alias: tag filter: - all addAllPagesToCollections: true # Warning: this is tag not the expected tags tag: someArbitraryTag --- {{ tag }}`); eleventyConfig.addTemplate("index.njk", `{{ collections.myCollection.length }}`); } }); let results = await elev.toJSON(); t.is(results.length, 5); t.is(results.filter((entry) => entry.url === "/")[0]?.content.trim(), "0") }); ================================================ FILE: test/Issue3831Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3831 Computed Data regression", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addGlobalData("eleventyComputed", { first_letter: function (data) { return data.title[0] } }); eleventyConfig.addTemplate("index.njk", `--- title: "Title" metadata: url: "/url/" eleventyComputed: "id": "{{ metadata.url }}glossary/entity/#webpage" --- {{ id }} {{ first_letter }}`); } }); let results = await elev.toJSON(); results.sort(); t.is(results.length, 1); t.is(results[0]?.content.trim(), `/url/glossary/entity/#webpage T`) }); ================================================ FILE: test/Issue3833Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3831 Computed Data regression", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `--- date: - April 1, 2025 ---`); } }); elev.disableLogger(); let e = await t.throwsAsync(() => elev.toJSON()); t.is(e.message, `Data cascade value for \`date\` (April 1, 2025) is invalid for ./test/noop/index.njk. Expected a JavaScript Date instance, luxon DateTime instance, or String value.`); }); ================================================ FILE: test/Issue3850Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3850 Computed Data regression part 2", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("index.njk", `--- site: download_link_mac: "http://example.com/" eleventyComputed: downloads: - links: - url: "{{ site.download_link_mac }}" --- {{ site.download_link_mac }}:::{{ downloads | dump | safe }}`); } }); let results = await elev.toJSON(); results.sort(); t.is(results.length, 1); t.is(results[0]?.content.trim(), `http://example.com/:::[{"links":[{"url":"http://example.com/"}]}]`) }); ================================================ FILE: test/Issue3853Test.js ================================================ import test from "ava"; import path from "node:path"; import { TemplatePath } from "@11ty/eleventy-utils"; import { spawnAsync } from "../src/Util/spawn.js"; test("#3853 absolute path input should strip output from permalink", async (t) => { let input = path.join(process.cwd(), "test/_issues/3853/deeper"); let output = path.join(process.cwd(), "test/_issues/3853/public/site"); let result = await spawnAsync( "node", ["../../../cmd.cjs", `--input=${input}`, `--output=${output}`, "--to=json"], { cwd: "test/_issues/3853/" } ); let json = JSON.parse(result); t.is(json.length, 1); t.is(json[0]?.outputPath, TemplatePath.standardizeFilePath("./public/site/index.html")); t.is(json[0]?.content.trim(), "3853"); }); ================================================ FILE: test/Issue3854Test.js ================================================ import test from "ava"; import { spawnAsync } from "../src/Util/spawn.js"; test("#3854 parent directory for content, with global data files", async (t) => { let result = await spawnAsync( "node", ["../../../../cmd.cjs", "--to=json"], { cwd: "test/_issues/3854/app/" } ); let json = JSON.parse(result); t.is(json.length, 2); json.sort((a, b) => { return a.inputPath.length - b.inputPath.length; }) t.is(json[0]?.content.trim(), "3854/parent"); t.is(json[1]?.content.trim(), "3854/child"); }); ================================================ FILE: test/Issue3860Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3860 addCollection consumes `collections` but is missing `collections.all`", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addFilter("keys", obj => Object.keys(obj)); eleventyConfig.addTemplate("post1.md", "# Post1", { tags: ["bar"]}); eleventyConfig.addTemplate("post2.md", "# Post2", { tags: ["foo"]}); eleventyConfig.addTemplate("tag.njk", "{{ collections | keys }}", { pagination: { data: "collections", size: 1, alias: "collection", }, permalink: "tag/{{collection}}/index.html", eleventyExcludeFromCollections: true, }); } }); let results = await elev.toJSON(); let tagPages = results.filter((entry) => entry.inputPath.endsWith("tag.njk")); t.is(tagPages.length, 3); t.is(tagPages[0]?.content.trim(), `bar,foo,all`) t.is(tagPages[1]?.content.trim(), `bar,foo,all`) t.is(tagPages[2]?.content.trim(), `bar,foo,all`) }); ================================================ FILE: test/Issue3870IncrementalTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; // This tests Eleventy Watch WITHOUT using the file system! test("#3870 templateRender has not yet initialized (not incremental)", async (t) => { let runs = [ { expected: `[]`, }, { expected: `[]`, }, ]; t.plan(runs.length + 1); let index = 0; let elev = new Eleventy("test/stubs-virtual/", "test/stubs-virtual/_site", { configPath: "test/stubs-virtual/eleventy.config.js", config(eleventyConfig) { eleventyConfig.addTemplate("search.11ty.js", class { data() { return { permalink: '/search.json', // permalink: false, layout: false, eleventyExcludeFromCollections: true, }; } async render(data) { return '[]'; } }); eleventyConfig.on("eleventy.after", ({ results }) => { t.is(results[0]?.content, runs[index].expected); }); } }); elev.disableLogger(); elev.setIncrementalBuild(true); await elev.init(); let asyncTriggerFn = await elev.watch(); for(let run of runs) { await asyncTriggerFn("test/stubs-virtual/eleventy.config.js"); index++; } await elev.stopWatch(); }); ================================================ FILE: test/Issue3870Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; // This tests Eleventy Watch WITHOUT using the file system! test("#3870 templateRender has not yet initialized (not incremental)", async (t) => { let runs = [ { expected: `[]`, }, { expected: `[]`, }, ]; t.plan(runs.length + 1); let index = 0; let elev = new Eleventy("test/stubs-virtual/", "test/stubs-virtual/_site", { configPath: "test/stubs-virtual/eleventy.config.js", config(eleventyConfig) { eleventyConfig.addTemplate("search.11ty.js", class { data() { return { permalink: '/search.json', // permalink: false, layout: false, eleventyExcludeFromCollections: true, }; } async render(data) { return '[]'; } }); eleventyConfig.on("eleventy.after", ({ results }) => { t.is(results[0]?.content, runs[index].expected); }); } }); elev.disableLogger(); await elev.init(); let asyncTriggerFn = await elev.watch(); for(let run of runs) { await asyncTriggerFn("test/stubs-virtual/eleventy.config.js"); index++; } await elev.stopWatch(); }); ================================================ FILE: test/Issue3875Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#3875 numeric tags", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addFilter("keys", (obj) => Object.keys(obj)); eleventyConfig.addTemplate("index.njk", "{{ collections | keys }}", { tags: [1,2,3] }); } }); let results = await elev.toJSON(); t.is(results[0].content, "1,2,3,all"); }); test("#3875 numeric tags (via front matter)", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addFilter("keys", (obj) => Object.keys(obj)); eleventyConfig.addTemplate("index.njk", `--- tags: - 1 - 2 - 3 --- {{ collections | keys }}`); } }); let results = await elev.toJSON(); t.is(results[0].content, "1,2,3,all"); }); test("#3875 consume a numeric tag collection (njk)", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addFilter("keyTypes", (obj) => Object.keys(obj).map(entry => typeof entry).join(",")); eleventyConfig.addTemplate("child.njk", "", { tags: [1] }); eleventyConfig.addTemplate("index.njk", `{{ collections | keyTypes }}:{{ collections[1].length }}:{{ collections['1'].length }}`); } }); let results = await elev.toJSON(); t.is(results.filter(entry => entry.inputPath.endsWith("index.njk"))[0].content, "string,string:1:1"); }); test("#3875 consume a numeric tag collection (liquid)", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addFilter("keyTypes", (obj) => Object.keys(obj).map(entry => typeof entry).join(",")); eleventyConfig.addTemplate("child.njk", "", { tags: [1] }); eleventyConfig.addTemplate("index.liquid", `{{ collections | keyTypes }}:{{ collections[1].length }}:{{ collections['1'].length }}`); } }); let results = await elev.toJSON(); t.is(results.filter(entry => entry.inputPath.endsWith("index.liquid"))[0].content, "string,string:1:1"); }); test("#3875 consume a numeric tag collection (11ty.js)", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addTemplate("child.njk", "", { tags: [1] }); eleventyConfig.addTemplate("index.11ty.js", { render(data) { return `${Object.keys(data.collections).map(entry => typeof entry).join(",")}:${data.collections[1].length}:${data.collections['1'].length}` } }); } }); let results = await elev.toJSON(); t.is(results.filter(entry => entry.inputPath.endsWith("index.11ty.js"))[0].content, "string,string:1:1"); }); ================================================ FILE: test/Issue434Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#434 Using `with context` to access collections", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.setIncludesDirectory("../stubs-434/_includes/"); eleventyConfig.addTemplate("index.njk", `{% import "macros.njk" as forms with context %}{{ forms.label('test') }}`); } }); let [result] = await elev.toJSON(); t.is(result.content, ""); }); test("#434 (not ideal) Filters in macros cannot access global data", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.setIncludesDirectory("../stubs-434/_includes/"); // This doesn’t work to fetch collections from macros eleventyConfig.addFilter("getCollection", function(name) { return this.collections?.[name] || this.ctx?.collections?.[name] || this.context?.environments?.collections?.[name]; }); eleventyConfig.addTemplate("index.njk", `{% import "macros-filter.njk" as forms %}{{ forms.label('test') }}`); } }); let [result] = await elev.toJSON(); t.is(result.content, ""); }); ================================================ FILE: test/Issue775Test.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("#775 Using data cascade in Collection API", async (t) => { let elev = new Eleventy("test/noop", false, { config(eleventyConfig) { eleventyConfig.addCollection("apic", collectionApi => { return collectionApi.getFilteredByTag("posts").filter(entry => { return entry.data.keep; }); }) eleventyConfig.addTemplate("post1.md", `# Header`, { tags: "posts", keep: true }); eleventyConfig.addTemplate("post2.md", `# Header`, { tags: "posts" }); eleventyConfig.addTemplate("post3.md", `# Header`, { tags: "posts" }); eleventyConfig.addTemplate("index.njk", `{{ collections.apic.length }}`); } }); let [result] = await elev.toJSON(); t.is(result.content, "1"); }); ================================================ FILE: test/JavaScriptDependenciesTest.js ================================================ import test from "ava"; import JavaScriptDependencies from "../src/Util/JavaScriptDependencies.js"; test("No node_modules", async (t) => { let deps = await JavaScriptDependencies.getDependencies([ "./test/stubs-dependency-tree/index.cjs", ]); t.deepEqual(deps, [ "./test/stubs-dependency-tree/child.cjs", "./test/stubs-dependency-tree/grandchild.cjs", ]); }); ================================================ FILE: test/JavaScriptFrontMatterTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; test("Custom Front Matter Parsing Options (using JavaScript node-retrieve-globals)", async (t) => { let elev = new Eleventy("./test/stubs/script-frontmatter/test.njk", "./_site"); elev.disableLogger(); let result = await elev.toJSON(); t.deepEqual(result.length, 1); t.is(result[0]?.content, `
Hi
Bye
`); }); test("Custom Front Matter Parsing Options (using JavaScript node-retrieve-globals), override project-wide front matter default.", async (t) => { let elev = new Eleventy("./test/stubs/script-frontmatter/test-default.njk", "./_site", { config: (eleventyConfig) => { eleventyConfig.setFrontMatterParsingOptions({ language: "js", }); }, }); elev.disableLogger(); let result = await elev.toJSON(); t.deepEqual(result.length, 1); t.is(result[0]?.content, `
Hi
Bye
`); }); test("Custom Front Matter Parsing Options (using backwards-compatible `js` instead of node-retrieve-globals)", async (t) => { let elev = new Eleventy("./test/stubs/script-frontmatter/test-js.njk", "./_site"); elev.disableLogger(); let result = await elev.toJSON(); t.deepEqual(result.length, 1); t.is(result[0]?.content, `
HELLO!
`); }); // https://github.com/11ty/eleventy/issues/3917 test("Issue #3917 previous JS object front matter shouldn’t have had implicit exports turned on", async (t) => { let elev = new Eleventy("./test/stubs-virtual-nowrite", "./test/stubs-virtual-nowrite/_site", { config: function(eleventyConfig) { eleventyConfig.addTemplate("test.njk", `---js { eleventyComputed: { summary: async function (data) { let textInsert = data ? 'something' : 'nothing'; return "Some text"; } } } --- Hello`); } }); elev.disableLogger(); let result = await elev.toJSON(); t.deepEqual(result.length, 1); t.is(result[0]?.content, `Hello`); }); ================================================ FILE: test/LayoutCacheTest.js ================================================ import test from "ava"; import templateCache from "../src/LayoutCache.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; test("Cache can save templates", async (t) => { templateCache.clear(); let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); templateCache.add(tmpl); t.is(templateCache.size(), 1); }); test("TemplateCache clear", async (t) => { templateCache.clear(); let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); templateCache.add(tmpl); t.is(templateCache.size(), 1); templateCache.clear(); t.is(templateCache.size(), 0); }); test("TemplateCache has", async (t) => { templateCache.clear(); let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); templateCache.add(tmpl); // Only TemplateLayout is cached t.is(templateCache.has("./test/stubs/template.liquid"), false); }); test("TemplateCache get success", async (t) => { templateCache.clear(); let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); templateCache.add(tmpl); // Only TemplateLayout is cached t.throws(() => { templateCache.get("./test/stubs/template.liquid"); }); }); test("TemplateCache get fail", async (t) => { templateCache.clear(); let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); templateCache.add(tmpl); t.throws(function () { templateCache.get("./test/stubs/template298374892.liquid"); }); }); ================================================ FILE: test/LodashTest.js ================================================ import test from "ava"; import lodash from "@11ty/lodash-custom"; import { ProxyWrap } from "../src/Util/Objects/ProxyWrap.js"; import { DeepFreeze } from "../src/Util/Objects/DeepFreeze.js"; const { set: lodashSet } = lodash; test("Lodash sanity tests", t => { t.deepEqual(lodashSet({}, "test", true), {"test": true}); t.deepEqual(lodashSet({}, "metadata.title", true), {"metadata": {title: true}}); }); test("Lodash set on proxy", t => { let target = {}; let obj = ProxyWrap(target, {}); let ret = lodashSet(obj, "metadata.title", "test"); t.deepEqual(ret, {metadata: {title: "test"}}); }); test("Lodash set on proxy object with data", t => { // let target = { metadata: { title: "default" } }; let target = {}; let fallback = {}; let obj = ProxyWrap(target, fallback); lodashSet(obj, "metadata.title", "test"); t.deepEqual(obj, {metadata: {title: "test"}}); }); // TODO re-add support for frozen fallbacks test.skip("Fallback is *not* mutated (is frozen) (does not exist in fallback)", t => { let fallback = {}; // oh my god freeze is shallow DeepFreeze(fallback); let target1 = ProxyWrap({ first: true, metadata: { a: 1 } }, fallback); let target2 = ProxyWrap({ second: true, metadata: { b: 1 } }, fallback); lodashSet(fallback, "metadata.c", 999); // does nothing lodashSet(target1, "metadata.c", 1); t.is(target1.metadata.c, 1); t.is(target2.metadata.c, undefined); t.deepEqual(target1, { first: true, metadata: { a: 1, c: 1}}); t.deepEqual(target2, { second: true, metadata: { b: 1 } }); }); // TODO re-add support for frozen fallbacks test.skip("Fallback is *not* mutated (is frozen) (exists in fallback)", t => { let fallback = { metadata: { d: 888 } }; // Object.freeze is shallow DeepFreeze(fallback); let target3 = ProxyWrap({ third: true }, fallback); t.deepEqual(target3, { third: true, metadata: { d: 888 }}); lodashSet(target3, "metadata.d", "all"); t.deepEqual(target3, { third: true, metadata: { d: "all" }}); }); ================================================ FILE: test/PaginationTest.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; import TemplateData from "../src/Data/TemplateData.js"; import Pagination from "../src/Plugins/Pagination.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getRenderedTemplates as getRenderedTmpls, renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; test("No data passed to pagination", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/notpaged.njk", "./test/stubs/", "./dist", ); let paging = new Pagination(tmpl, {}, tmpl.config); t.is(paging.pagedItems.length, 0); t.is((await paging.getPageTemplates()).length, 0); }); test("No pagination", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/notpaged.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let paging = new Pagination(tmpl, data, tmpl.config); paging.setTemplate(tmpl); t.falsy(data.pagination); t.is(paging.getPageCount(), 0); t.is(paging.pagedItems.length, 0); t.is((await paging.getPageTemplates()).length, 0); }); test("Empty paged data", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/paged-empty.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let paging = new Pagination(tmpl, data, tmpl.config); paging.setTemplate(tmpl); t.is(paging.getPageCount(), 0); t.is(paging.pagedItems.length, 0); t.is((await paging.getPageTemplates()).length, 0); }); test("Empty paged data with generatePageOnEmptyData enabled", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/paged-empty-pageonemptydata.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let paging = new Pagination(tmpl, data, tmpl.config); paging.setTemplate(tmpl); t.is(paging.getPageCount(), 1); t.is(paging.pagedItems.length, 1); t.is((await paging.getPageTemplates()).length, 1); }); test("Pagination enabled in frontmatter", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedresolve.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let paging = new Pagination(tmpl, data, tmpl.config); paging.setTemplate(tmpl); t.truthy(data.testdata); t.truthy(data.testdata.sub); t.truthy(data.pagination); t.is(data.pagination.data, "testdata.sub"); t.is(paging.getPageCount(), 2); t.is(data.pagination.size, 4); }); test("Resolve paged data in frontmatter", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedresolve.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let paging = new Pagination(tmpl, data, tmpl.config); paging.setTemplate(tmpl); t.is(paging._resolveItems().length, 8); t.is(paging.getPageCount(), 2); t.is(paging.pagedItems.length, 2); }); test("Paginate data in frontmatter", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedinlinedata.njk", "./test/stubs/", "./dist", ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 2); t.is(pages[0].outputPath, "./dist/paged/pagedinlinedata/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item4
" ); t.is(pages[1].outputPath, "./dist/paged/pagedinlinedata/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item5
  2. item6
  3. item7
  4. item8
" ); }); test("Paginate external data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/paged/paged.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); // local data t.truthy(data.items.sub.length); let pages = await tmpl.getTemplates(data); t.is(pages.length, 2); t.is(pages[0].outputPath, "./dist/paged/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item4
  5. item5
" ); t.is(pages[1].outputPath, "./dist/paged/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item6
  2. item7
  3. item8
" ); }); test("Permalink with pagination variables", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalink.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/paged/slug-candidate/index.html"); t.is(pages[1].outputPath, "./dist/paged/another-slug-candidate/index.html"); }); test("Permalink with pagination variables (numeric)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinknumeric.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.truthy(pages[0].data.pagination.firstPageLink); t.truthy(pages[0].data.pagination.firstPageHref); t.truthy(pages[0].data.pagination.lastPageLink); t.truthy(pages[0].data.pagination.lastPageHref); t.is(pages[0].outputPath, "./dist/paged/page-0/index.html"); t.falsy(pages[0].data.pagination.previousPageLink); t.is(pages[0].data.pagination.nextPageLink, "/paged/page-1/index.html"); t.is(pages[0].data.pagination.nextPageHref, "/paged/page-1/"); t.is(pages[0].data.pagination.pageLinks.length, 2); t.is(pages[0].data.pagination.links.length, 2); t.is(pages[0].data.pagination.hrefs.length, 2); t.is(pages[1].outputPath, "./dist/paged/page-1/index.html"); t.is(pages[1].data.pagination.previousPageLink, "/paged/page-0/index.html"); t.is(pages[1].data.pagination.previousPageHref, "/paged/page-0/"); t.falsy(pages[1].data.pagination.nextPageLink); t.is(pages[1].data.pagination.pageLinks.length, 2); t.is(pages[1].data.pagination.links.length, 2); t.is(pages[1].data.pagination.hrefs.length, 2); }); test("Permalink with pagination variables (numeric, one indexed)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinknumericoneindexed.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/paged/page-1/index.html"); t.falsy(pages[0].data.pagination.previousPageLink); t.is(pages[0].data.pagination.nextPageLink, "/paged/page-2/index.html"); t.is(pages[0].data.pagination.nextPageHref, "/paged/page-2/"); t.is(pages[0].data.pagination.pageLinks.length, 2); t.is(pages[0].data.pagination.links.length, 2); t.is(pages[0].data.pagination.hrefs.length, 2); t.is(pages[1].outputPath, "./dist/paged/page-2/index.html"); t.is(pages[1].data.pagination.previousPageLink, "/paged/page-1/index.html"); t.is(pages[1].data.pagination.previousPageHref, "/paged/page-1/"); t.falsy(pages[1].data.pagination.nextPageLink); t.is(pages[1].data.pagination.pageLinks.length, 2); t.is(pages[1].data.pagination.links.length, 2); t.is(pages[1].data.pagination.hrefs.length, 2); }); test("Permalink first and last page link with pagination variables (numeric)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinknumeric.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].data.pagination.firstPageLink, "/paged/page-0/index.html"); t.is(pages[0].data.pagination.lastPageLink, "/paged/page-1/index.html"); t.is(pages[1].data.pagination.firstPageLink, "/paged/page-0/index.html"); t.is(pages[1].data.pagination.lastPageLink, "/paged/page-1/index.html"); }); test("Permalink first and last page link with pagination variables (numeric, one indexed)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinknumericoneindexed.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].data.pagination.firstPageLink, "/paged/page-1/index.html"); t.is(pages[0].data.pagination.lastPageLink, "/paged/page-2/index.html"); t.is(pages[1].data.pagination.firstPageLink, "/paged/page-1/index.html"); t.is(pages[1].data.pagination.lastPageLink, "/paged/page-2/index.html"); }); test("Alias to page data", async (t) => { let tmpl = await getNewTemplate("./test/stubs/paged/pagedalias.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/pagedalias/item1/index.html"); t.is(pages[1].outputPath, "./dist/pagedalias/item2/index.html"); t.is((await renderTemplate(pages[0].template, pages[0].data)).trim(), "item1"); t.is((await renderTemplate(pages[1].template, pages[1].data)).trim(), "item2"); }); test("Alias to page data (size 2)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedaliassize2.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/pagedalias/item1/index.html"); t.is(pages[1].outputPath, "./dist/pagedalias/item3/index.html"); t.is((await renderTemplate(pages[0].template, pages[0].data)).trim(), "item1"); t.is((await renderTemplate(pages[1].template, pages[1].data)).trim(), "item3"); }); test("Permalink with pagination variables (and an if statement, nunjucks)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinkif.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/paged/index.html"); t.is(pages[1].outputPath, "./dist/paged/page-1/index.html"); }); test("Permalink with pagination variables (and an if statement, liquid)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinkif.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/paged/index.html"); t.is(pages[1].outputPath, "./dist/paged/page-1/index.html"); }); test("Template with Pagination", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinkif.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let outputPath = await tmpl.getOutputPath(data); t.is(outputPath, "./dist/paged/index.html"); let templates = await getRenderedTmpls(tmpl, data); t.is(templates.length, 2); }); test("Issue 135", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/issue-135/template.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); let templates = await getRenderedTmpls(tmpl, data); t.is(data.articles.length, 1); t.is(data.articles[0].title, "Do you even paginate bro?"); t.is(await templates[0].outputPath, "./dist/blog/do-you-even-paginate-bro/index.html"); let pages = await tmpl.getTemplates(data); t.is(pages.length, 1); t.is(pages[0].outputPath, "./dist/blog/do-you-even-paginate-bro/index.html"); }); test("Template with Pagination, getTemplates has page variables set", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinkif.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.page.url, "/paged/"); t.is(templates[0].data.page.outputPath, "./dist/paged/index.html"); t.is(templates[1].data.page.url, "/paged/page-1/"); t.is(templates[1].data.page.outputPath, "./dist/paged/page-1/index.html"); }); test("Template with Pagination, has page variables set", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedpermalinkif.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].data.page.url, "/paged/"); t.is(pages[0].data.page.outputPath, "./dist/paged/index.html"); t.is(pages[1].data.page.url, "/paged/page-1/"); t.is(pages[1].data.page.outputPath, "./dist/paged/page-1/index.html"); }); test("Page over an object (use keys)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/paged/pagedobject.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 3); t.is(pages[0].outputPath, "./dist/paged/pagedobject/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item4
" ); t.is(pages[1].outputPath, "./dist/paged/pagedobject/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item5
  2. item6
  3. item7
  4. item8
" ); }); test("Page over an object (use values)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedobjectvalues.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 3); t.is(pages[0].outputPath, "./dist/paged/pagedobjectvalues/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. itemvalue1
  2. itemvalue2
  3. itemvalue3
  4. itemvalue4
" ); t.is(pages[1].outputPath, "./dist/paged/pagedobjectvalues/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. itemvalue5
  2. itemvalue6
  3. itemvalue7
  4. itemvalue8
" ); }); test("Page over an object (filtered, array)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedobjectfilterarray.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item5
" ); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item6
  2. item7
  3. item8
  4. item9
" ); }); test("Page over an object (filtered, string)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedobjectfilterstring.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 2); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item5
" ); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item6
  2. item7
  3. item8
  4. item9
" ); }); test("Pagination with deep data merge #147", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let tmpl = await getNewTemplate( "./test/stubs/paged/pagedinlinedata.njk", "./test/stubs/", "./dist", null, null, eleventyConfig ); tmpl.config.keys.layout = "layout"; let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 2); t.is(pages[0].outputPath, "./dist/paged/pagedinlinedata/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item1
  2. item2
  3. item3
  4. item4
" ); t.is(pages[1].outputPath, "./dist/paged/pagedinlinedata/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item5
  2. item6
  3. item7
  4. item8
" ); }); test("Pagination with deep data merge with alias #147", async (t) => { let tmpl = await getNewTemplate("./test/stubs/paged/pagedalias.njk", "./test/stubs/", "./dist"); tmpl.config.dynamicPermalinks = true; let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages[0].outputPath, "./dist/pagedalias/item1/index.html"); t.is(pages[1].outputPath, "./dist/pagedalias/item2/index.html"); t.is((await renderTemplate(pages[0].template, pages[0].data)).trim(), "item1"); t.is((await renderTemplate(pages[1].template, pages[1].data)).trim(), "item2"); }); test("Paginate data in frontmatter (reversed)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedinlinedata-reverse.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await tmpl.getTemplates(data); t.is(pages.length, 2); t.is(pages[0].outputPath, "./dist/paged/pagedinlinedata-reverse/index.html"); t.is( (await renderTemplate(pages[0].template, pages[0].data)).trim(), "
  1. item8
  2. item7
  3. item6
  4. item5
" ); t.is(pages[1].outputPath, "./dist/paged/pagedinlinedata-reverse/1/index.html"); t.is( (await renderTemplate(pages[1].template, pages[1].data)).trim(), "
  1. item4
  2. item3
  3. item2
  4. item1
" ); }); test("No circular dependency (does not throw)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); new Pagination( null, { collections: { tag1: [], }, pagination: { data: "collections.tag1", size: 1, }, tags: ["tag2"], }, eleventyConfig ); t.true(true); }); test("Circular dependency (pagination iterates over tag1 but also supplies pages to tag1)", async (t) => { await t.throwsAsync(async () => { let eleventyConfig = await getTemplateConfigInstance(); new Pagination( null, { collections: { tag1: [], tag2: [], }, pagination: { data: "collections.tag1", size: 1, }, tags: ["tag1"], }, eleventyConfig ); }); }); test("Circular dependency but should not error because it uses eleventyExcludeFromCollections", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); new Pagination( null, { eleventyExcludeFromCollections: true, collections: { tag1: [], tag2: [], }, pagination: { data: "collections.tag1", size: 1, }, tags: ["tag1"], }, eleventyConfig ); t.true(true); }); test("Pagination `before` Callback", async (t) => { let tmpl = await getNewTemplate("./test/stubs/paged/paged-before.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.deepEqual(templates[0].data.pagination.items, ["item6"]); t.deepEqual(templates[0].data.myalias, "item6"); }); test("Pagination `before` Callback with metadata", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/paged-before-metadata.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.deepEqual(templates[0].data.pagination.items, ["item3"]); }); test("Pagination `before` Callback with a Filter", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/paged-before-filter.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.deepEqual(templates[0].data.pagination.items, ["item2"]); t.deepEqual(templates[0].data.myalias, "item2"); }); test("Pagination `before` Callback with `reverse: true` (test order of operations)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/paged-before-and-reverse.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.deepEqual(templates[0].data.pagination.items, ["item2"]); }); test("Pagination new v0.10.0 href/hrefs", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/paged/paged.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.pagination.hrefs.length, 2); t.truthy(templates[0].data.pagination.href.first); t.truthy(templates[0].data.pagination.href.last); t.falsy(templates[0].data.pagination.href.previous); t.truthy(templates[0].data.pagination.href.next); t.is(templates[1].data.pagination.hrefs.length, 2); t.truthy(templates[1].data.pagination.href.first); t.truthy(templates[1].data.pagination.href.last); t.truthy(templates[1].data.pagination.href.previous); t.falsy(templates[1].data.pagination.href.next); }); test("Pagination new v0.10.0 page/pages", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/paged/paged.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.pagination.pages.length, 2); t.is(templates[0].data.pagination.pages[0].length, 5); t.is(templates[0].data.pagination.pages[1].length, 3); t.truthy(templates[0].data.pagination.page.first); t.truthy(templates[0].data.pagination.page.last); t.falsy(templates[0].data.pagination.page.previous); t.truthy(templates[0].data.pagination.page.next); t.is(templates[1].data.pagination.pages.length, 2); t.is(templates[1].data.pagination.pages[0].length, 5); t.is(templates[1].data.pagination.pages[1].length, 3); t.truthy(templates[1].data.pagination.page.first); t.truthy(templates[1].data.pagination.page.last); t.truthy(templates[1].data.pagination.page.previous); t.falsy(templates[1].data.pagination.page.next); }); test("Pagination new v0.10.0 alias", async (t) => { let tmpl = await getNewTemplate("./test/stubs/paged/pagedalias.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.pagination.alias, "font.test"); t.is(templates[1].data.pagination.alias, "font.test"); }); test("Pagination make sure pageNumber is numeric for {{ pageNumber + 1 }} Issue #760", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/paged/pagedinlinedata.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.pagination.pageNumber, 0); t.not(templates[0].data.pagination.pageNumber, "0"); }); test("Pagination mutable global data", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/paged-global-data-mutable/", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/paged-global-data-mutable/paged-differing-data-set.njk", "./test/stubs/paged-global-data-mutable/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates.length, 3); t.deepEqual(templates[0].data.pagination.items[0], { key1: "item1", key2: "item2", }); t.deepEqual(templates[1].data.pagination.items[0], { key3: "item3", key4: "item4", }); t.deepEqual(templates[2].data.pagination.items[0], { key5: "item5", key6: "item6", }); t.deepEqual(templates[0].data.item, { key1: "item1", key2: "item2" }); t.deepEqual(templates[1].data.item, { key3: "item3", key4: "item4" }); t.deepEqual(templates[2].data.item, { key5: "item5", key6: "item6" }); }); test("Pagination template/dir data files run once, Issue 919", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-919", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs-919/test.njk", "./test/stubs-919/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates.length, 3); t.is(templates[0].data.test, templates[1].data.test); t.is(templates[1].data.test, templates[2].data.test); }); test("Pagination and eleventyComputed permalink, issue #1555 and #1865", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/pagination-eleventycomputed-permalink.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.page.url, "/venues/first/"); t.is(templates[1].data.page.url, "/venues/second/"); t.is(templates[2].data.page.url, "/venues/third/"); }); test("Pagination and eleventyComputed data, issues #2512, #2837, #3013", async (t) => { let templateLangs = ["liquid", "html", "md", "njk"]; let apostrophe = { liquid: "'", html: "'", md: "'", hbs: "&#x27;", mustache: "&#39;", njk: "&#39;", }; for (let lang of templateLangs) { let msg = `lang: ${lang}`; let le = lang === "md" ? "\n" : ""; let elev = new Eleventy(`./test/stubs-3013/${lang}/`, `./test/stubs-3013/${lang}/_site`, { source: "cli", runMode: "build", }); await elev.init(); let written = await elev.toJSON(); t.is(written[0].url, "/paul-mescal/", msg); t.is(written[0].content, `The Effervescent adventures of Paul Mescal${le}`, msg); t.is(written[1].url, "/populace-and-power/", msg); t.is( written[1].content, `Populace and Power: A user${apostrophe[lang]}s guide${le}`, msg ); } }); ================================================ FILE: test/PassthroughCopyBehaviorTest.js ================================================ import test from "ava"; import checkPassthroughCopyBehavior from "../src/Util/PassthroughCopyBehaviorCheck.js"; test("Standard use", (t) => { t.is( checkPassthroughCopyBehavior( { serverPassthroughCopyBehavior: "passthrough", serverOptions: {}, }, "serve" ), true ); }); test("Config fallback", (t) => { t.is( checkPassthroughCopyBehavior( { serverPassthroughCopyBehavior: "copy", serverOptions: {}, }, "serve" ), false ); }); test("Other dev server", (t) => { t.is( checkPassthroughCopyBehavior( { serverPassthroughCopyBehavior: "passthrough", serverOptions: { module: "somethingelse", }, }, "serve" ), false ); }); test("Non --serve run modes", (t) => { t.is( checkPassthroughCopyBehavior( { serverPassthroughCopyBehavior: "passthrough", serverOptions: {}, }, "watch" ), false ); t.is( checkPassthroughCopyBehavior( { serverPassthroughCopyBehavior: "passthrough", serverOptions: {}, }, "build" ), false ); }); ================================================ FILE: test/PathNormalizerTest.js ================================================ import test from "ava"; import path from "path"; import PathNormalizer from "../src/Util/PathNormalizer.js"; test("normalizeSeparator", (t) => { t.is(PathNormalizer.normalizeSeperator("."), "."); t.is(PathNormalizer.normalizeSeperator("a/b"), "a/b"); t.is(PathNormalizer.normalizeSeperator("a\\b").replace(/\\/g, path.sep), "a/b"); t.is(PathNormalizer.normalizeSeperator("a\\b/c").replace(/\\/g, path.sep), "a/b/c"); t.is(PathNormalizer.normalizeSeperator(undefined), undefined); }); test("getParts", (t) => { t.deepEqual(PathNormalizer.getParts("."), []); t.deepEqual(PathNormalizer.getParts("test/a/b"), ["test", "a", "b"]); t.deepEqual(PathNormalizer.getParts("test\\a\\b".replace(/\\/g, path.sep)), ["test", "a", "b"]); }); test("getAllPaths", (t) => { t.deepEqual(PathNormalizer.getAllPaths("."), []); t.deepEqual(PathNormalizer.getAllPaths("test/a/b"), ["test", "test/a", "test/a/b"]); t.deepEqual(PathNormalizer.getAllPaths("test/a/b.liquid"), ["test", "test/a", "test/a/b.liquid"]); }); ================================================ FILE: test/PathPrefixer.js ================================================ import test from "ava"; import path from "path"; import PathPrefixer from "../src/Util/PathPrefixer.js"; test("joinUrlParts", (t) => { t.is(PathPrefixer.joinUrlParts("a"), "a"); t.is(PathPrefixer.joinUrlParts("a", "b"), "a/b"); t.is(PathPrefixer.joinUrlParts("", "a", "b"), "a/b"); t.is(PathPrefixer.joinUrlParts("/a", "b"), "/a/b"); t.is(PathPrefixer.joinUrlParts("a", "b", "c"), "a/b/c"); t.is(PathPrefixer.joinUrlParts("a/b", "c/"), "a/b/c/"); }); test("joinUrlParts (Windows)", (t) => { // The replace calls are needed, since "\" is a valid path char on unix t.is(PathPrefixer.joinUrlParts("a"), "a"); t.is(PathPrefixer.joinUrlParts("a\\b".replace(/\\/g, path.sep)), "a/b"); t.is(PathPrefixer.joinUrlParts("\\a\\b".replace(/\\/g, path.sep)), "/a/b"); t.is(PathPrefixer.joinUrlParts("a\\b\\c".replace(/\\/g, path.sep)), "a/b/c"); t.is(PathPrefixer.joinUrlParts("a\\b".replace(/\\/g, path.sep), "c"), "a/b/c"); t.is(PathPrefixer.joinUrlParts("a\\b\\c\\".replace(/\\/g, path.sep)), "a/b/c/"); t.is(PathPrefixer.joinUrlParts("a\\b/c\\".replace(/\\/g, path.sep)), "a/b/c/"); }); test("normalizePathPrefix", (t) => { t.is(PathPrefixer.normalizePathPrefix("a"), "/a"); t.is(PathPrefixer.normalizePathPrefix("a/b"), "/a/b"); t.is(PathPrefixer.normalizePathPrefix("/a/b"), "/a/b"); t.is(PathPrefixer.normalizePathPrefix("/"), "/"); t.is(PathPrefixer.normalizePathPrefix(""), "/"); t.is(PathPrefixer.normalizePathPrefix(undefined), "/"); }); ================================================ FILE: test/PluralizeTest.js ================================================ import test from "ava"; import pluralize from "../src/Util/Pluralize.js"; test("Pluralize", (t) => { t.is(pluralize(0, "test", "tests"), "tests"); t.is(pluralize(1, "test", "tests"), "test"); t.is(pluralize(2, "test", "tests"), "tests"); t.is(pluralize(3, "test", "tests"), "tests"); t.is(pluralize(3.5, "test", "tests"), "tests"); }); ================================================ FILE: test/PreserveClosingTagsPluginTest.js ================================================ import test from "ava"; import { PreserveClosingTagsPlugin } from "../src/Plugins/PreserveClosingTagsPlugin.js"; import Eleventy from "../src/Eleventy.js"; test("Using the PreserveClosingTagsPlugin plugin (meta off) #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(PreserveClosingTagsPlugin); eleventyConfig.addTemplate("test.njk", ``, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, ``); }); test("Using the PreserveClosingTagsPlugin plugin (meta on) #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(PreserveClosingTagsPlugin, { tags: ["meta"] }); eleventyConfig.addTemplate("test.njk", ``, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, ``); }); test("Using the PreserveClosingTagsPlugin plugin (meta on, link off) #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(PreserveClosingTagsPlugin, { tags: ["meta"] }); eleventyConfig.addTemplate("test.njk", ``, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, ``); }); test("Using the PreserveClosingTagsPlugin plugin (meta on, link on) #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(PreserveClosingTagsPlugin, { tags: ["meta", "link"] }); eleventyConfig.addTemplate("test.njk", ``, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, ``); }); test("Using the PreserveClosingTagsPlugin plugin (meta on, link on, title off) #3356", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { config: function (eleventyConfig) { eleventyConfig.addPlugin(PreserveClosingTagsPlugin, { tags: ["meta", "link"] }); eleventyConfig.addTemplate("test.njk", `My Title`, {}); }, }); let results = await elev.toJSON(); t.is(results[0].content, `My Title`); }); ================================================ FILE: test/ProjectDirectoriesTest.js ================================================ import test from "ava"; import ProjectDirectories from "../src/Util/ProjectDirectories.js"; test("Implied input", t => { let d = new ProjectDirectories(); t.is(d.input, "./"); t.is(d.inputFile, undefined); }); test("Input matches", t => { let d = new ProjectDirectories(); d.setInput("./test/"); t.is(d.input, "./test/"); t.is(d.inputFile, undefined); }); test("Normalized input (has trailing slash)", t => { let d = new ProjectDirectories(); d.setInput("test/"); t.is(d.input, "./test/"); t.is(d.inputFile, undefined); }); test("Normalized input (no trailing slash)", t => { let d = new ProjectDirectories(); d.setInput("test"); t.is(d.input, "./test/"); t.is(d.inputFile, undefined); }); test("Input must exist", t => { let d = new ProjectDirectories(); t.throws(() => d.setInput("does-not-exist")); }); test("Input as file", t => { let d = new ProjectDirectories(); d.setInput("test/stubs/index.html"); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, "./test/stubs/index.html"); }); test("Input as file (deep)", t => { let d = new ProjectDirectories(); d.setInput("test/stubs/img/stub.md"); t.is(d.input, "./test/stubs/img/"); t.is(d.inputFile, "./test/stubs/img/stub.md"); }); test("Input as file (deep with inputDir)", t => { let d = new ProjectDirectories(); d.setInput("test/stubs/img/stub.md", "test/stubs"); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, "./test/stubs/img/stub.md"); }); test("Data directory, implied input and default data", t => { let d = new ProjectDirectories(); t.is(d.data, "./_data/"); }); test("Data directory matches, explicit input", t => { let d = new ProjectDirectories(); d.setInput("./test/"); t.is(d.data, "./test/_data/"); }); test("Data directory matches, explicit input and data", t => { let d = new ProjectDirectories(); d.setInput("./test/"); d.setData("mydata"); t.is(d.data, "./test/mydata/"); }); test("Data directory matches, explicit input and data (order reversed)", t => { let d = new ProjectDirectories(); d.setData("mydata"); d.setInput("./test/"); t.is(d.data, "./test/mydata/"); }); test("includes implied, layouts are not", t => { let d = new ProjectDirectories(); t.is(d.layouts, undefined); t.is(d.includes, "./_includes/"); }); test("Layouts/includes, explicit", t => { let d = new ProjectDirectories(); d.setLayouts("layouts"); d.setIncludes("includes"); t.is(d.layouts, "./layouts/"); t.is(d.includes, "./includes/"); d.setInput("test"); t.is(d.layouts, "./test/layouts/"); t.is(d.includes, "./test/includes/"); d.setLayouts("../layouts"); d.setIncludes("../includes"); t.is(d.layouts, "./layouts/"); t.is(d.includes, "./includes/"); }); test("Output, implied", t => { let d = new ProjectDirectories(); t.is(d.output, "./_site/"); }); test("Content/template/input paths", t => { let d = new ProjectDirectories(); t.is(d.getInputPath("test.md"), "./test.md"); t.is(d.getInputPath("./test.md"), "./test.md"); t.is(d.getLayoutPath("./layout.html"), "./_includes/layout.html"); d.setInput("test"); t.is(d.getInputPath("test.md"), "./test/test.md"); t.is(d.getInputPath("./test.md"), "./test/test.md"); t.is(d.getLayoutPath("./layout.html"), "./test/_includes/layout.html"); }); test("Project file paths", t => { let d = new ProjectDirectories(); t.is(d.getProjectPath("eleventy.config.js"), "./eleventy.config.js"); t.is(d.getProjectPath("./eleventy.config.js"), "./eleventy.config.js"); }); test("Input could be a glob!", t => { let d = new ProjectDirectories(); d.setInput("./test/*.md"); t.is(d.input, "./test/"); t.is(d.inputFile, undefined); }); test("Setting values via config object", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", output: "dist", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./dist/"); t.is(d.data, "./test/stubs/_data/"); t.is(d.includes, "./test/stubs/_includes/"); t.is(d.layouts, undefined); }); test("Setting values via config object (input relative dirs)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", data: "globaldata", layouts: "mylayouts", includes: "components", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./_site/"); t.is(d.data, "./test/stubs/globaldata/"); t.is(d.includes, "./test/stubs/components/"); t.is(d.layouts, "./test/stubs/mylayouts/"); }); test("Setting values via config object (input relative dirs, parent dirs)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", data: "../globaldata", includes: "../components", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./_site/"); t.is(d.data, "./test/globaldata/"); t.is(d.includes, "./test/components/"); t.is(d.layouts, undefined); }); test("Setting values via config object (eleventy-base-blog example)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "src", includes: "../_includes", data: "../_data", output: "_site" }); t.is(d.input, "./src/"); t.is(d.inputFile, undefined); t.is(d.output, "./_site/"); t.is(d.data, "./_data/"); t.is(d.includes, "./_includes/"); t.is(d.layouts, undefined); }); test("Setting values via config object (empty string/false value)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", output: "_site", data: false, // falsy supported for output, data, includes, and layouts (uses input dir) includes: "", // layouts will be undefined when excluded here }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./_site/"); t.is(d.data, "./test/stubs/"); t.is(d.includes, "./test/stubs/"); t.is(d.layouts, undefined); }); test("Setting values via config object (layouts is set but falsy)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", output: "_site", layouts: "", // falsy supported for output, data, includes, and layouts (uses input dir) }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./_site/"); t.is(d.data, "./test/stubs/_data/"); t.is(d.includes, "./test/stubs/_includes/"); t.is(d.layouts, "./test/stubs/"); }); test("Setting values via config object (output is falsy)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", output: "", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./"); t.is(d.data, "./test/stubs/_data/"); t.is(d.includes, "./test/stubs/_includes/"); t.is(d.layouts, undefined); }); test("Setting values via config object (dots)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", output: ".", includes: ".", layouts: ".", data: ".", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./"); t.is(d.data, "./test/stubs/"); t.is(d.includes, "./test/stubs/"); t.is(d.includes, "./test/stubs/"); }); test("CLI values should override all others (both)", t => { let d = new ProjectDirectories(); d.setInput("src"); d.setOutput("dist"); d.freeze(); d.setViaConfigObject({ input: "test/stubs", includes: "myincludes", }); t.is(d.input, "./src/"); t.is(d.inputFile, undefined); t.is(d.output, "./dist/"); t.is(d.data, "./src/_data/"); t.is(d.includes, "./src/myincludes/"); t.is(d.layouts, undefined); }); test("CLI values should override all others (just input)", t => { let d = new ProjectDirectories(); d.setInput("src"); d.freeze(); d.setViaConfigObject({ input: "test/stubs", includes: "myincludes", // always okay, not a CLI param output: "dist", }); t.is(d.input, "./src/"); t.is(d.inputFile, undefined); t.is(d.output, "./dist/"); t.is(d.data, "./src/_data/"); t.is(d.includes, "./src/myincludes/"); t.is(d.layouts, undefined); }); test("CLI values should override all others (just output)", t => { let d = new ProjectDirectories(); d.setOutput("dist"); d.freeze(); d.setViaConfigObject({ input: "test/stubs", includes: "myincludes", // always okay, not a CLI param output: "someotherdir", }); t.is(d.input, "./test/stubs/"); t.is(d.inputFile, undefined); t.is(d.output, "./dist/"); t.is(d.data, "./test/stubs/_data/"); t.is(d.includes, "./test/stubs/myincludes/"); t.is(d.layouts, undefined); }); test("getLayoutPath (layouts dir)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", layouts: "mylayouts", includes: "components", }); t.is(d.getLayoutPath("layout.html"), "./test/stubs/mylayouts/layout.html"); t.is(d.getLayoutPathRelativeToInputDirectory("layout.html"), "mylayouts/layout.html"); }); test("getLayoutPath (includes dir)", t => { let d = new ProjectDirectories(); d.setViaConfigObject({ input: "test/stubs", includes: "components", }); t.is(d.getLayoutPath("layout.html"), "./test/stubs/components/layout.html"); t.is(d.getLayoutPathRelativeToInputDirectory("layout.html"), "components/layout.html"); }); test("isFileIn*Folder", t => { let d = new ProjectDirectories(); t.is(d.isFileInProjectFolder("test.njk"), true); t.is(d.isFileInProjectFolder("../test.njk"), false); t.is(d.isFileInOutputFolder("test.njk"), false); t.is(d.isFileInOutputFolder("../test.njk"), false); t.is(d.isFileInOutputFolder("../_site/test.html"), false); t.is(d.isFileInOutputFolder("_site/test.html"), true); }); test("isFileInOutputFolder (change output folder)", t => { let d = new ProjectDirectories(); d.setOutput("yolo") t.is(d.isFileInOutputFolder("test.njk"), false); t.is(d.isFileInOutputFolder("../test.njk"), false); t.is(d.isFileInOutputFolder("_site/test.html"), false); t.is(d.isFileInOutputFolder("../_site/test.html"), false); t.is(d.isFileInOutputFolder("yolo/test.html"), true); }); ================================================ FILE: test/ProjectTemplateFormatsTest.js ================================================ import test from "ava"; import ProjectTemplateFormats from "../src/Util/ProjectTemplateFormats.js"; function getTestInstance() { let tf = new ProjectTemplateFormats(); return tf; } test("Empty formats", t => { let tf = new ProjectTemplateFormats(); t.deepEqual(tf.getTemplateFormats(), []); }); test("Return all eligible on no config or CLI", t => { let tf = getTestInstance(); t.deepEqual(tf.getTemplateFormats(), []); }); // CLI test("CLI", t => { let tf = getTestInstance(); tf.setViaCommandLine("md,html"); t.deepEqual(tf.getTemplateFormats(), ["md", "html"]); }); test("CLI empty", t => { let tf = getTestInstance(); tf.setViaCommandLine(""); t.deepEqual(tf.getTemplateFormats(), []); }); test("CLI *", t => { let tf = getTestInstance(); tf.setViaCommandLine("*"); t.deepEqual(tf.getTemplateFormats(), []); }); // Config Set test("Config", t => { let tf = getTestInstance(); tf.setViaConfig("md,html"); t.deepEqual(tf.getTemplateFormats(), ["md", "html"]); }); test("Config empty", t => { let tf = getTestInstance(); tf.setViaConfig(""); t.deepEqual(tf.getTemplateFormats(), []); }); test("Config *", t => { let tf = getTestInstance(); tf.setViaConfig("*"); t.deepEqual(tf.getTemplateFormats(), []); }); // Config Add test("Config Add", t => { let tf = getTestInstance(); // add without set unions all with new tf.addViaConfig("md,html"); t.deepEqual(tf.getTemplateFormats(), ["md", "html"]); }); test("Config Add (not yet known)", t => { let tf = getTestInstance(); // add without set unions all with new tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); test("Config Add empty", t => { let tf = getTestInstance(); tf.addViaConfig(""); t.deepEqual(tf.getTemplateFormats(), []); }); test("Config Add *", t => { let tf = getTestInstance(); t.throws(() => { tf.addViaConfig("*"); }, { message: '`addTemplateFormats("*")` is not supported for project template syntaxes.' }); }); test("Config Add Multiple", t => { let tf = getTestInstance(); // While this does support multiple addTemplateFormat calls from config, they are collected and addViaConfig is only called once. // add without set unions all with new tf.addViaConfig("vue"); tf.addViaConfig("pug"); tf.addViaConfig("zbbbbb"); t.deepEqual(tf.getTemplateFormats(), ["zbbbbb"]); }); // CLI and Config (CLI wins every time) test("CLI + Config", t => { let tf = getTestInstance(); tf.setViaCommandLine("md,html"); tf.setViaConfig("liquid"); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["md", "html"]); }); test("CLI + Config empty", t => { let tf = getTestInstance(); tf.setViaCommandLine(""); tf.setViaConfig("liquid"); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), []); }); test("CLI + Config *", t => { let tf = getTestInstance(); tf.setViaCommandLine("*"); tf.setViaConfig("liquid"); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["liquid", "vue"]); }); // Config set and add test("Config set/add", t => { let tf = getTestInstance(); tf.setViaConfig("liquid"); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["liquid", "vue"]); }); test("Config set/add set undefined", t => { let tf = getTestInstance(); tf.setViaConfig(undefined); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); test("Config set/add add undefined", t => { let tf = getTestInstance(); tf.setViaConfig("vue"); tf.addViaConfig(undefined); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); test("Config set/add set empty", t => { let tf = getTestInstance(); tf.setViaConfig(""); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); test("Config set/add add empty", t => { let tf = getTestInstance(); tf.setViaConfig("vue"); tf.addViaConfig(""); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); test("Config set/add both empty", t => { let tf = getTestInstance(); tf.setViaConfig(""); tf.addViaConfig(""); t.deepEqual(tf.getTemplateFormats(), []); }); test("Config set *, add", t => { let tf = getTestInstance(); tf.setViaConfig("*"); tf.addViaConfig("vue"); t.deepEqual(tf.getTemplateFormats(), ["vue"]); }); ================================================ FILE: test/ProxyWrapTest.js ================================================ import test from "ava"; import { ProxyWrap } from "../src/Util/Objects/ProxyWrap.js"; test("Basic wrap", (t) => { let test = ProxyWrap({}, { a: 1 }); t.is(test.a, 1); }); test("Nested wrap", (t) => { let test = ProxyWrap({}, { child: { a: 1, b: 1 } }); t.truthy(test.child); t.deepEqual(test.child, { a: 1, b: 1 }); t.deepEqual(test.child.a, 1); t.deepEqual(test.child.b, 1); }); test("Double nested wrap", (t) => { let child = { a: 1, b: 1, c: { grandchild: 1, }, }; let test = ProxyWrap( {}, { child, } ); t.truthy(test.child); t.deepEqual(test.child, child); t.deepEqual(test.child.a, 1); t.deepEqual(test.child.b, 1); t.deepEqual(test.child.c.grandchild, 1); }); test("Array", (t) => { let test = ProxyWrap({}, { child: [1, 2, 3] }); t.deepEqual(test.child, [1, 2, 3]); t.deepEqual(test.child[1], 2); }); test("Array nested", (t) => { let test = ProxyWrap({}, { child: [1, [2], 3] }); t.deepEqual(test.child, [1, [2], 3]); t.deepEqual(test.child[1], [2]); t.deepEqual(test.child[1][0], 2); }); test("Fails for invalid target", (t) => { t.throws(() => ProxyWrap(true, {})); }); test("Fails for invalid fallback", (t) => { t.throws(() => ProxyWrap({}, true)); }); test("Frozen Object", (t) => { let test = ProxyWrap({}, Object.freeze({ eleventy: { generator: "Eleventy v3.0.0" } })); t.deepEqual(test.eleventy.generator, "Eleventy v3.0.0"); }); test("Frozen Nested Object", (t) => { let test = ProxyWrap({ eleventy: {} }, { eleventy: Object.freeze({ generator: "Eleventy v3.0.0" }) }); t.deepEqual(test.eleventy.generator, "Eleventy v3.0.0"); }); ================================================ FILE: test/ReservedDataTest.js ================================================ import test from "ava"; import ReservedData from "../src/Util/ReservedData.js"; import Eleventy from "../src/Eleventy.js"; test("No reserved Keys", t => { t.deepEqual(ReservedData.getReservedKeys({ key: {} }).sort(), []); }); test("Standard keys are reserved", t => { t.deepEqual(ReservedData.getReservedKeys({ content: "" }).sort(), ["content"]); t.deepEqual(ReservedData.getReservedKeys({ collections: {} }).sort(), ["collections"]); t.deepEqual(ReservedData.getReservedKeys({ content: "", collections: {} }).sort(), ["collections", "content"]); }); test("`page` subkeys", t => { t.deepEqual(ReservedData.getReservedKeys({ page: {} }).sort(), []); t.deepEqual(ReservedData.getReservedKeys({ page: "" }).sort(), ["page"]); t.deepEqual(ReservedData.getReservedKeys({ page: { date: "", otherkey: "" } }).sort(), ["page.date"]); t.deepEqual(ReservedData.getReservedKeys({ page: { inputPath: "", otherkey: "" } }).sort(), ["page.inputPath"]); t.deepEqual(ReservedData.getReservedKeys({ page: { fileSlug: "", otherkey: "" } }).sort(), ["page.fileSlug"]); t.deepEqual(ReservedData.getReservedKeys({ page: { filePathStem: "", otherkey: "" } }).sort(), ["page.filePathStem"]); t.deepEqual(ReservedData.getReservedKeys({ page: { outputFileExtension: "", otherkey: "" } }).sort(), ["page.outputFileExtension"]); t.deepEqual(ReservedData.getReservedKeys({ page: { templateSyntax: "", otherkey: "" } }).sort(), ["page.templateSyntax"]); t.deepEqual(ReservedData.getReservedKeys({ page: { url: "", otherkey: "" } }).sort(), ["page.url"]); t.deepEqual(ReservedData.getReservedKeys({ page: { outputPath: "", otherkey: "" } }).sort(), ["page.outputPath"]); t.deepEqual(ReservedData.getReservedKeys({ page: { date: "", outputPath: "", otherkey: "" } }).sort(), ["page.date", "page.outputPath"]); }); test("Eleventy freeze data set via config API throws error (page)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { configPath: "./test/stubs-virtual/eleventy.config.js", config: eleventyConfig => { eleventyConfig.addGlobalData("page", "lol no"); eleventyConfig.addTemplate("index.html", ``); } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: page (source: ./test/stubs-virtual/eleventy.config.js). You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy freeze data set via config API throws error (eleventy)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { configPath: "./test/stubs-virtual/eleventy.config.js", config: eleventyConfig => { eleventyConfig.addGlobalData("eleventy", "lol no"); eleventyConfig.addTemplate("index.html", ``); } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: eleventy (source: ./test/stubs-virtual/eleventy.config.js). You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy freeze data set global data file throws error (page)", async (t) => { let elev = new Eleventy({ input: "./test/stubs-freeze/page/", config: eleventyConfig => { eleventyConfig.addTemplate("index.html", ``); } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: page.url (source: ./test/stubs-freeze/page/_data/page.js). You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); test("Eleventy freeze data set global data file throws error (eleventy)", async (t) => { let elev = new Eleventy({ input: "./test/stubs-freeze/eleventy/", config: eleventyConfig => { eleventyConfig.addTemplate("index.html", ``); } }); elev.disableLogger(); await t.throwsAsync(() => elev.toJSON(), { message: 'You attempted to set one of Eleventy’s reserved data property names: eleventy (source: ./test/stubs-freeze/eleventy/_data/eleventy.js). You can opt-out of this behavior with `eleventyConfig.setFreezeReservedData(false)` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. `eleventy`, `pkg`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/' }); }); ================================================ FILE: test/SemverCheckTest.js ================================================ import test from "ava"; import semver from "semver"; test("Satisfies sanity checks with beta/canary", (t) => { t.true( semver.satisfies("1.0.0-beta.1", ">= 0.7.0", { includePrerelease: true, }), "1.0 Beta is valid for 0.x" ); t.true( semver.satisfies("1.0.0-canary.1", ">= 0.7.0", { includePrerelease: true, }), "1.0 Canary is valid for 0.x" ); t.true( semver.satisfies("1.0.0-beta.1", ">= 0.7.0", { includePrerelease: true, }), "1.0 Beta is valid for 0.x" ); t.true( semver.satisfies("1.0.0", ">= 0.7.0", { includePrerelease: true, }), "1.0 is valid for 0.x" ); // keep canary around, it won’t have the `includePrerelease` option in `UserConfig->versionCheck` t.true( semver.satisfies("1.0.0", ">=0.7 || >=1.0.0-canary", { includePrerelease: true, }) ); t.true( semver.satisfies("1.0.0-beta.1", ">=0.7 || >=1.0.0-canary", { includePrerelease: true, }) ); t.true( semver.satisfies("1.0.0-canary.1", ">=0.7 || >=1.0.0-canary", { includePrerelease: true, }) ); }); ================================================ FILE: test/SortableTest.js ================================================ import test from "ava"; import { DateTime } from "luxon"; import Sortable from "../src/Util/Objects/Sortable.js"; test("get Sort Function", (t) => { let s = new Sortable(); t.deepEqual(s.getSortFunction(), Sortable.sortFunctionAlphabeticAscending); }); test("Alphabetic Ascending", (t) => { let s = new Sortable(); t.false(s.isSortNumeric); t.true(s.isSortAscending); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sort(), ["a", "m", "z"]); }); test("Alphabetic Ascending (shortcut)", (t) => { let s = new Sortable(); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sortAscending(), ["a", "m", "z"]); }); test("Alphabetic Descending", (t) => { let s = new Sortable(); s.setSortDescending(); t.false(s.isSortNumeric); t.false(s.isSortAscending); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sort(), ["z", "m", "a"]); }); test("Alphabetic Descending (shortcut)", (t) => { let s = new Sortable(); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sortDescending(), ["z", "m", "a"]); }); test("Numeric Ascending", (t) => { let s = new Sortable(); s.setSortNumeric(true); t.true(s.isSortNumeric); t.true(s.isSortAscending); s.add(1); s.add(4); s.add(2); t.deepEqual(s.sort(), [1, 2, 4]); }); test("Numeric Descending", (t) => { let s = new Sortable(); s.setSortNumeric(true); s.setSortDescending(); t.true(s.isSortNumeric); t.false(s.isSortAscending); s.add(1); s.add(4); s.add(2); t.deepEqual(s.sort(), [4, 2, 1]); }); test("Date Assumptions", (t) => { t.is(DateTime.fromISO("2007-10-10") - new Date(2007, 9, 10).getTime(), 0); t.is(DateTime.fromISO("2008-10-10") - new Date(2008, 9, 10).getTime(), 0); t.not(DateTime.fromISO("2008-10-10") - new Date(2007, 9, 10).getTime(), 0); }); test("Date and Sortable Assumptions", (t) => { // Sortable works here without extra code because Luxon’s valueOf works in equality comparison (for alphabetic lists) t.is( Sortable.sortFunctionAlphabeticAscending( DateTime.fromISO("2007-10-10"), new Date(2007, 9, 10).getTime() ), 0 ); t.is( Sortable.sortFunctionAlphabeticDescending( DateTime.fromISO("2007-10-10"), new Date(2007, 9, 10).getTime() ), 0 ); t.is( Sortable.sortFunctionAlphabeticAscending( DateTime.fromISO("2008-10-10"), new Date(2007, 9, 10).getTime() ), 1 ); t.is( Sortable.sortFunctionAlphabeticDescending( DateTime.fromISO("2008-10-10"), new Date(2007, 9, 10).getTime() ), -1 ); // Sortable works here without extra code because Luxon’s valueOf works in subtraction (for numeric lists) t.is( Sortable.sortFunctionNumericAscending( DateTime.fromISO("2008-10-10"), new Date(2008, 9, 10).getTime() ), 0 ); t.is( Sortable.sortFunctionNumericDescending( DateTime.fromISO("2008-10-10"), new Date(2008, 9, 10).getTime() ), 0 ); t.true( Sortable.sortFunctionNumericAscending( DateTime.fromISO("2008-10-10"), new Date(2007, 9, 10).getTime() ) > 0 ); t.true( Sortable.sortFunctionNumericDescending( DateTime.fromISO("2008-10-10"), new Date(2007, 9, 10).getTime() ) < 0 ); }); test("Date Ascending", (t) => { let s = new Sortable(); let date1 = DateTime.fromISO("2007-10-10"); let date2 = DateTime.fromISO("2008-10-10"); let date3 = DateTime.fromISO("2009-10-10"); s.add(date3); s.add(date2); s.add(date1); t.deepEqual(s.sort(), [date1, date2, date3]); }); test("Date Descending", (t) => { let s = new Sortable(); s.setSortDescending(); let date1 = DateTime.fromISO("2007-10-10"); let date2 = DateTime.fromISO("2008-10-10"); let date3 = DateTime.fromISO("2009-10-10"); s.add(date2); s.add(date3); s.add(date1); t.deepEqual(s.sort(), [date3, date2, date1]); }); test("Alphabetic Ascending (short str sort arg)", (t) => { let s = new Sortable(); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sort("A-Z"), ["a", "m", "z"]); }); test("Alphabetic Descending (short str sort arg)", (t) => { let s = new Sortable(); s.add("a"); s.add("z"); s.add("m"); t.deepEqual(s.sort("Z-A"), ["z", "m", "a"]); }); test("Invalid Sort Function Name", (t) => { let s = new Sortable(); t.throws(() => s.sort("INVALID SORT STRING")); }); test("Ascending / Descending Sorting Setters (asc, no param)", (t) => { let s = new Sortable(); s.isSortAscending = false; s.setSortAscending(); t.is(s.isSortAscending, true); }); test("Ascending / Descending Sorting Setters (desc, no param)", (t) => { let s = new Sortable(); s.isSortAscending = true; s.setSortDescending(); t.is(s.isSortAscending, false); }); test("Ascending / Descending Sorting Setters (asc, true)", (t) => { let s = new Sortable(); s.isSortAscending = false; s.setSortAscending(true); t.is(s.isSortAscending, true); }); test("Ascending / Descending Sorting Setters (asc, false)", (t) => { let s = new Sortable(); s.isSortAscending = true; s.setSortAscending(false); t.is(s.isSortAscending, false); }); test("Ascending / Descending Sorting Setters (desc, true)", (t) => { let s = new Sortable(); s.isSortAscending = true; s.setSortDescending(true); t.is(s.isSortAscending, false); }); test("Ascending / Descending Sorting Setters (desc, false)", (t) => { let s = new Sortable(); s.isSortAscending = false; s.setSortDescending(false); t.is(s.isSortAscending, true); }); ================================================ FILE: test/TemplateCollectionTest.js ================================================ import test from "ava"; import { isGlobMatch } from "../src/Util/GlobMatcher.js"; import Collection from "../src/TemplateCollection.js"; import Sortable from "../src/Util/Objects/Sortable.js"; import getNewTemplateForTests from "../test/_getNewTemplateForTests.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; function getNewTemplate(filename, input, output, eleventyConfig) { return getNewTemplateForTests(filename, input, output, null, null, eleventyConfig); } function getNewTemplateByNumber(num, eleventyConfig) { let extensions = ["md", "md", "md", "md", "md", "html", "njk", "md", "md", "md"]; return getNewTemplateForTests( `./test/stubs/collection/test${num}.${extensions[num - 1]}`, "./test/stubs/", "./test/stubs/_site", null, null, eleventyConfig, ); } async function addTemplate(collection, template) { let data = await template.getData(); for (let map of await template.getTemplates(data)) { collection.add(map); } } test("Basic setup", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl2); await addTemplate(c, tmpl3); t.is(c.length, 3); }); test("sortFunctionDate", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl4); await addTemplate(c, tmpl5); let posts = c.sort(Sortable.sortFunctionDate); t.is(posts.length, 3); t.deepEqual(posts[0].template, tmpl4); t.deepEqual(posts[1].template, tmpl1); t.deepEqual(posts[2].template, tmpl5); }); test("sortFunctionDateInputPath", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl4); await addTemplate(c, tmpl5); let posts = c.sort(Sortable.sortFunctionDateInputPath); t.is(posts.length, 3); t.deepEqual(posts[0].template, tmpl4); t.deepEqual(posts[1].template, tmpl1); t.deepEqual(posts[2].template, tmpl5); }); test("getFilteredByTag", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl2); await addTemplate(c, tmpl3); let posts = c.getFilteredByTag("post"); t.is(posts.length, 2); t.deepEqual(posts[0].template, tmpl1); t.deepEqual(posts[1].template, tmpl3); let cats = c.getFilteredByTag("cat"); t.is(cats.length, 2); t.deepEqual(cats[0].template, tmpl2); t.deepEqual(cats[1].template, tmpl3); let dogs = c.getFilteredByTag("dog"); t.is(dogs.length, 1); t.deepEqual(dogs[0].template, tmpl1); }); test("getFilteredByTag (added out of order, sorted)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl3); await addTemplate(c, tmpl2); await addTemplate(c, tmpl1); let posts = c.getFilteredByTag("post"); t.is(posts.length, 2); t.deepEqual(posts[0].template, tmpl1); t.deepEqual(posts[1].template, tmpl3); let cats = c.getFilteredByTag("cat"); t.truthy(cats.length); t.is(cats.length, 2); t.deepEqual(cats[0].template, tmpl2); t.deepEqual(cats[1].template, tmpl3); let dogs = c.getFilteredByTag("dog"); t.truthy(dogs.length); t.deepEqual(dogs[0].template, tmpl1); }); test("getFilteredByTags", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl2); await addTemplate(c, tmpl3); let postsAndCats = c.getFilteredByTags("post", "cat"); t.is(postsAndCats.length, 1); t.deepEqual(postsAndCats[0].template, tmpl3); let cats = c.getFilteredByTags("cat"); t.is(cats.length, 2); t.deepEqual(cats[0].template, tmpl2); t.deepEqual(cats[1].template, tmpl3); let dogs = c.getFilteredByTags("dog"); t.is(dogs.length, 1); t.deepEqual(dogs[0].template, tmpl1); }); test("getFilteredByTags (added out of order, sorted)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl3); await addTemplate(c, tmpl2); await addTemplate(c, tmpl1); let postsAndCats = c.getFilteredByTags("post", "cat"); t.truthy(postsAndCats.length); t.is(postsAndCats.length, 1); t.deepEqual(postsAndCats[0].template, tmpl3); let cats = c.getFilteredByTags("cat"); t.truthy(cats.length); t.is(cats.length, 2); t.deepEqual(cats[0].template, tmpl2); t.deepEqual(cats[1].template, tmpl3); let dogs = c.getFilteredByTags("dog"); t.truthy(dogs.length); t.is(dogs.length, 1); t.deepEqual(dogs[0].template, tmpl1); }); test("getFilteredByGlob", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl6 = await getNewTemplateByNumber(6, eleventyConfig); let tmpl7 = await getNewTemplateByNumber(7, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl6); await addTemplate(c, tmpl7); let markdowns = c.getFilteredByGlob("./**/*.md"); t.is(markdowns.length, 1); t.deepEqual(markdowns[0].template, tmpl1); }); test("getFilteredByGlob no dash dot", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl6 = await getNewTemplateByNumber(6, eleventyConfig); let tmpl7 = await getNewTemplateByNumber(7, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl6); await addTemplate(c, tmpl7); let markdowns = c.getFilteredByGlob("**/*.md"); t.is(markdowns.length, 1); t.deepEqual(markdowns[0].template, tmpl1); let htmls = c.getFilteredByGlob("**/*.{html,njk}"); t.is(htmls.length, 2); t.deepEqual(htmls[0].template, tmpl6); t.deepEqual(htmls[1].template, tmpl7); }); test("partial match on tag string, issue 95", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let cat = await getNewTemplate( "./test/stubs/issue-95/cat.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let notacat = await getNewTemplate( "./test/stubs/issue-95/notacat.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let c = new Collection(); await addTemplate(c, cat); await addTemplate(c, notacat); let posts = c.getFilteredByTag("cat"); t.is(posts.length, 1); }); // Swapped to `micromatch` in 3.0.0-alpha.17, and later to `picomatch`. // The test can stay as a sanity check. test("micromatch assumptions, issue #127", async (t) => { function isMatch(filepath, globs) { return isGlobMatch(filepath, globs); } t.true( isMatch("src/bookmarks/test.md", ["**/+(bookmarks|posts|screencasts)/**/!(index)*.md"]), ); t.true( isMatch("./src/bookmarks/test.md", ["./**/+(bookmarks|posts|screencasts)/**/!(index)*.md"]), ); let c = new Collection(); let globs = c.getGlobs("**/+(bookmarks|posts|screencasts)/**/!(index)*.md"); t.deepEqual(globs, ["./**/+(bookmarks|posts|screencasts)/**/!(index)*.md"]); t.true(isMatch("./src/bookmarks/test.md", globs)); t.false(isMatch("./src/bookmarks/index.md", globs)); t.false(isMatch("./src/bookmarks/index2.md", globs)); t.true(isMatch("./src/_content/bookmarks/2018-03-27-git-message.md", globs)); }); test("Sort in place (issue #352)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl4); await addTemplate(c, tmpl5); let posts = c.getAllSorted(); t.is(posts.length, 3); t.deepEqual(posts[0].template, tmpl4); t.deepEqual(posts[1].template, tmpl1); t.deepEqual(posts[2].template, tmpl5); let posts2 = c.getAllSorted().reverse(); t.is(posts2.length, 3); t.deepEqual(posts2[0].template, tmpl5); t.deepEqual(posts2[1].template, tmpl1); t.deepEqual(posts2[2].template, tmpl4); let posts3 = c.getAllSorted().reverse(); t.is(posts3.length, 3); t.deepEqual(posts3[0].template, tmpl5); t.deepEqual(posts3[1].template, tmpl1); t.deepEqual(posts3[2].template, tmpl4); }); test("getFilteredByTag with excludes", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl8 = await getNewTemplateByNumber(8, eleventyConfig); let tmpl9 = await getNewTemplateByNumber(9, eleventyConfig); let tmpl10 = await getNewTemplateByNumber(10, eleventyConfig); let c = new Collection(); await addTemplate(c, tmpl8); await addTemplate(c, tmpl9); await addTemplate(c, tmpl10); let posts = c.getFilteredByTag("post"); t.is(posts.length, 0); let offices = c.getFilteredByTag("office"); offices.sort(Sortable.sortFunctionDate); t.is(offices.length, 2); t.deepEqual(offices[0].template, tmpl10); t.deepEqual(offices[1].template, tmpl9); }); ================================================ FILE: test/TemplateConfigTest.js ================================================ import test from "ava"; import md from "markdown-it"; import TemplateConfig from "../src/TemplateConfig.js"; import defaultConfig from "../src/defaultConfig.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; test("Template Config local config overrides base config", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.is(cfg.markdownTemplateEngine, "njk"); t.is(cfg.templateFormats.join(","), "md,njk"); // merged, not overwritten t.true(Object.keys(cfg.keys).length > 1); t.truthy(Object.keys(cfg.nunjucksFilters).length); t.is(Object.keys(cfg.transforms).length, 4); t.is( cfg.transforms.prettyHtml(`
`, "test.html"), `
`, ); }); test("Add liquid tag", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addLiquidTag("myTagName", function () {}); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidTags).indexOf("myTagName"), -1); }); test("Add nunjucks tag", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addNunjucksTag("myNunjucksTag", function () {}); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksTags).indexOf("myNunjucksTag"), -1); }); test("Add nunjucks global", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addNunjucksGlobal("myNunjucksGlobal1", function () {}); templateCfg.userConfig.addNunjucksGlobal("myNunjucksGlobal2", 42); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksGlobals).indexOf("myNunjucksGlobal1"), -1); t.not(Object.keys(cfg.nunjucksGlobals).indexOf("myNunjucksGlobal2"), -1); }); test("Add liquid filter", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addLiquidFilter("myFilterName", function (liquidEngine) { return {}; }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("myFilterName"), -1); }); test("Add nunjucks filter", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addNunjucksFilter("myFilterName", function () {}); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test("Add universal filter", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addFilter("myFilterName", function () {}); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("myFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test("Add namespaced universal filter", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("testNamespace", function () { templateCfg.userConfig.addFilter("MyFilterName", function () {}); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("testNamespaceMyFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNamespaceMyFilterName"), -1); }); test("Add namespaced universal filter using underscore", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("testNamespace_", function () { templateCfg.userConfig.addFilter("myFilterName", function () {}); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("testNamespace_myFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNamespace_myFilterName"), -1); }); test("Add namespaced plugin", async (t) => { let templateCfg = new TemplateConfig(); templateCfg.userConfig.namespace("testNamespace", function () { templateCfg.userConfig.addPlugin(function (eleventyConfig) { eleventyConfig.addFilter("MyFilterName", function () {}); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("testNamespaceMyFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNamespaceMyFilterName"), -1); }); test("Add namespaced plugin using underscore", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("testNamespace_", function () { templateCfg.userConfig.addPlugin(function (config) { config.addFilter("myFilterName", function () {}); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("testNamespace_myFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNamespace_myFilterName"), -1); }); test("Empty namespace", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("", function () { templateCfg.userConfig.addNunjucksFilter("myFilterName", function () {}); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test("Nested Empty Inner namespace", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("testNs", function () { templateCfg.userConfig.namespace("", function () { templateCfg.userConfig.addNunjucksFilter("myFilterName", function () {}); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNsmyFilterName"), -1); }); test("Nested Empty Outer namespace", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace("", function () { templateCfg.userConfig.namespace("testNs", function () { templateCfg.userConfig.addNunjucksFilter("myFilterName", function () {}); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNsmyFilterName"), -1); }); // important for backwards compatibility with old // `module.exports = function (eleventyConfig, pluginNamespace) {` // plugin code test("Non-string namespaces are ignored", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.namespace(["lkdsjflksd"], function () { templateCfg.userConfig.addNunjucksFilter("myFilterName", function () {}); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test(".addPlugin oddity: I don’t think pluginNamespace was ever passed in here, but we don’t want this to break", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addPlugin(function (eleventyConfig, pluginNamespace) { eleventyConfig.namespace(pluginNamespace, () => { eleventyConfig.addNunjucksFilter("myFilterName", function () {}); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test("Test url universal filter with custom pathPrefix (no slash)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.setPathPrefix("/testdirectory/"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.is(cfg.pathPrefix, "/testdirectory/"); }); test("setTemplateFormats(string)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); // 0.11.0 removes dupes templateCfg.userConfig.setTemplateFormats("njk, liquid, njk"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["njk", "liquid"]); }); test("setTemplateFormats(array)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats(["njk", "liquid"]); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["njk", "liquid"]); }); test("setTemplateFormats(array, size 1)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats(["liquid"]); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["liquid"]); }); test("setTemplateFormats(empty array)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats([]); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, []); }); test("setTemplateFormats(null)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats(null); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual([...cfg.templateFormats].sort(), ["md", "njk"]); }); test("setTemplateFormats(undefined)", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats(undefined); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual([...cfg.templateFormats].sort(), ["md", "njk"]); }); test("multiple setTemplateFormats calls", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setTemplateFormats("njk"); templateCfg.userConfig.setTemplateFormats("pug"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["pug"]); }); test("addTemplateFormats()", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addTemplateFormats("vue"); await templateCfg.init(); let cfg = templateCfg.getConfig(); // should have ALL of the original defaults t.deepEqual(cfg.templateFormats, ["md", "njk", "vue"]); }); test("addTemplateFormats() via Plugin", async (t) => { let templateCfg = new TemplateConfig(); templateCfg.userConfig.addTemplateFormats("pug"); templateCfg.userConfig.addPlugin(cfg => { cfg.addTemplateFormats("webc"); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["liquid", "md", "njk", "html", "11ty.js", "pug", "webc"]); }); test("both setTemplateFormats and addTemplateFormats", async (t) => { // Template Formats can come from three places // defaultConfig.js config API (not used yet) // defaultConfig.js config return object // project config file config API // project config file config return object let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addTemplateFormats("vue"); templateCfg.userConfig.setTemplateFormats("pug"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.templateFormats, ["pug", "vue"]); }); test("addTemplateFormats() Array", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addTemplateFormats("vue2"); templateCfg.userConfig.addTemplateFormats(["vue"]); templateCfg.userConfig.addTemplateFormats(["text", "txt"]); await templateCfg.init(); let cfg = templateCfg.getConfig(); // should have ALL of the original defaults t.deepEqual(cfg.templateFormats, ["md", "njk", "vue2", "vue", "text", "txt"]); }); test("libraryOverrides", async (t) => { let mdLib = md(); let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.setLibrary("md", mdLib); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.falsy(cfg.libraryOverrides.ldkja); t.falsy(cfg.libraryOverrides.njk); t.truthy(cfg.libraryOverrides.md); t.deepEqual(mdLib, cfg.libraryOverrides.md); }); test("addGlobalData", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addGlobalData("function", () => new Date()); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.globalData).indexOf("function"), -1); }); test("Properly throws error on missing module #182", async (t) => { await t.throwsAsync(async () => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/broken-config.cjs"); await templateCfg.init(); templateCfg.getConfig(); }); }); test("Properly throws error when config returns a Promise", async (t) => { await t.throwsAsync(async () => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config-promise.js"); await templateCfg.init(); templateCfg.getConfig(); }); }); test(".addWatchTarget adds a watch target", async (t) => { let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config.cjs"); templateCfg.userConfig.addWatchTarget("/testdirectory/"); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.deepEqual(cfg.additionalWatchTargets, ["/testdirectory/"]); }); test("Nested .addPlugin calls", async (t) => { t.plan(2); let templateCfg = new TemplateConfig(); templateCfg.userConfig.addPlugin(function OuterPlugin(eleventyConfig) { t.truthy(true); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { t.truthy(true); }); }); await templateCfg.init(); templateCfg.getConfig(); }); test("Nested .addPlugin calls (×3)", async (t) => { t.plan(3); let templateCfg = new TemplateConfig(); templateCfg.userConfig.addPlugin(function OuterPlugin(eleventyConfig) { t.truthy(true); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { t.truthy(true); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { t.truthy(true); }); }); }); await templateCfg.init(); templateCfg.getConfig(); }); test("Nested .addPlugin calls order", async (t) => { t.plan(3); let templateCfg = new TemplateConfig(); let order = []; templateCfg.userConfig.addPlugin(function OuterPlugin(eleventyConfig) { order.push(1); t.deepEqual(order, [1]); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push(2); t.deepEqual(order, [1, 2]); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push(3); t.deepEqual(order, [1, 2, 3]); }); }); }); await templateCfg.init(); templateCfg.getConfig(); }); test("Nested .addPlugin calls. More complex order", async (t) => { t.plan(5); let templateCfg = new TemplateConfig(); let order = []; templateCfg.userConfig.addPlugin(function OuterPlugin(eleventyConfig) { order.push("1"); t.deepEqual(order, ["1"]); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push("2"); t.deepEqual(order, ["1", "2"]); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push("3a"); t.deepEqual(order, ["1", "2", "3a"]); }); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push("3b"); t.deepEqual(order, ["1", "2", "3a", "3b"]); }); }); eleventyConfig.addPlugin(function InnerPlugin(eleventyConfig) { order.push("2b"); t.deepEqual(order, ["1", "2", "3a", "3b", "2b"]); }); }); await templateCfg.init(); templateCfg.getConfig(); }); test(".addPlugin has access to pathPrefix", async (t) => { t.plan(1); let templateCfg = new TemplateConfig(); templateCfg.userConfig.addPlugin(function (eleventyConfig) { t.is(eleventyConfig.pathPrefix, "/"); }); await templateCfg.init(); templateCfg.getConfig(); }); test(".addPlugin has access to pathPrefix (override method)", async (t) => { t.plan(1); let templateCfg = new TemplateConfig(); templateCfg.setPathPrefix("/test/"); templateCfg.userConfig.addPlugin(function (eleventyConfig) { t.is(eleventyConfig.pathPrefix, "/test/"); }); await templateCfg.init(); templateCfg.getConfig(); }); test("falsy pathPrefix should fall back to default", async (t) => { t.plan(1); let templateCfg = new TemplateConfig(defaultConfig, "./test/stubs/config-empty-pathprefix.cjs"); templateCfg.userConfig.addPlugin(function (eleventyConfig) { t.is(eleventyConfig.pathPrefix, "/"); }); await templateCfg.init(); templateCfg.getConfig(); }); test("Add async plugin", async (t) => { let templateCfg = new TemplateConfig(); await templateCfg.userConfig.addPlugin(async (eleventyConfig) => { await new Promise((resolve) => { setTimeout(() => { eleventyConfig.addFilter("myFilterName", function () {}); resolve(); }, 10); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("myFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("myFilterName"), -1); }); test("Async namespace", async (t) => { let templateCfg = new TemplateConfig(); await templateCfg.userConfig.namespace("testNamespace", async (eleventyConfig) => { await new Promise((resolve) => { setTimeout(() => { eleventyConfig.addFilter("MyFilterName", function () {}); resolve(); }, 10); }); }); await templateCfg.init(); let cfg = templateCfg.getConfig(); t.not(Object.keys(cfg.liquidFilters).indexOf("testNamespaceMyFilterName"), -1); t.not(Object.keys(cfg.nunjucksFilters).indexOf("testNamespaceMyFilterName"), -1); }); test("ProjectDirectories instance exists in user accessible config", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let cfg = eleventyConfig.getConfig(); t.truthy(cfg.directories); t.is(cfg.directories.input, "./"); t.is(cfg.directories.data, "./_data/"); t.is(cfg.directories.includes, "./_includes/"); t.is(cfg.directories.layouts, undefined); t.is(cfg.directories.output, "./_site/"); t.throws(() => { cfg.directories.input = "should not work"; }); t.throws(() => { cfg.directories.data = "should not work"; }); t.throws(() => { cfg.directories.includes = "should not work"; }); t.throws(() => { cfg.directories.layouts = "should not work"; }); t.throws(() => { cfg.directories.output = "should not work"; }); }); test("Test getters #3310", async (t) => { let templateCfg = new TemplateConfig(); let userCfg = templateCfg.userConfig; userCfg.addShortcode("myShortcode", function () {}); userCfg.addShortcode("myAsyncShortcode", async function () {}); userCfg.addPairedShortcode("myPairedShortcode", function () {}); userCfg.addPairedShortcode("myPairedAsyncShortcode", async function () {}); userCfg.addFilter("myFilter", function () {}); userCfg.addFilter("myAsyncFilter", async function () {}); userCfg.addPlugin(function (eleventyConfig) { eleventyConfig.addFilter("myPluginFilter", function () {}); }); await templateCfg.init(); let filterNames = Object.keys(userCfg.getFilters()); t.true(filterNames.includes("myFilter")); t.true(filterNames.includes("myAsyncFilter")); t.true(filterNames.includes("myPluginFilter")); let filterNamesSync = Object.keys(userCfg.getFilters({ type: "sync" })); t.true(filterNamesSync.includes("myFilter")); t.false(filterNamesSync.includes("myAsyncFilter")); t.true(filterNamesSync.includes("myPluginFilter")); let filterNamesAsync = Object.keys(userCfg.getFilters({ type: "async" })); t.false(filterNamesAsync.includes("myFilter")); t.true(filterNamesAsync.includes("myAsyncFilter")); t.false(filterNamesAsync.includes("myPluginFilter")); t.truthy(userCfg.getFilter("myFilter")); t.truthy(userCfg.getFilter("myAsyncFilter")); t.truthy(userCfg.getFilter("myPluginFilter")); let shortcodeNames = Object.keys(userCfg.getShortcodes()); t.true(shortcodeNames.includes("myShortcode")); t.true(shortcodeNames.includes("myAsyncShortcode")); let shortcodeNamesSync = Object.keys(userCfg.getShortcodes({ type: "sync" })); t.true(shortcodeNamesSync.includes("myShortcode")); t.false(shortcodeNamesSync.includes("myAsyncShortcode")); let shortcodeNamesAsync = Object.keys(userCfg.getShortcodes({ type: "async" })); t.false(shortcodeNamesAsync.includes("myShortcode")); t.true(shortcodeNamesAsync.includes("myAsyncShortcode")); t.truthy(userCfg.getShortcode("myShortcode")); t.truthy(userCfg.getShortcode("myAsyncShortcode")); let pairedShortcodeNames = Object.keys(userCfg.getPairedShortcodes()); t.true(pairedShortcodeNames.includes("myPairedShortcode")); t.true(pairedShortcodeNames.includes("myPairedAsyncShortcode")); let pairedShortcodeNamesSync = Object.keys(userCfg.getPairedShortcodes({ type: "sync" })); t.true(pairedShortcodeNamesSync.includes("myPairedShortcode")); t.false(pairedShortcodeNamesSync.includes("myPairedAsyncShortcode")); let pairedShortcodeNamesAsync = Object.keys(userCfg.getPairedShortcodes({ type: "async" })); t.false(pairedShortcodeNamesAsync.includes("myPairedShortcode")); t.true(pairedShortcodeNamesAsync.includes("myPairedAsyncShortcode")); t.truthy(userCfg.getPairedShortcode("myPairedShortcode")); t.truthy(userCfg.getPairedShortcode("myPairedAsyncShortcode")); }); ================================================ FILE: test/TemplateDataTest.js ================================================ import test from "ava"; import semver from "semver"; import { createRequire } from "module"; import { Merge } from "@11ty/eleventy-utils"; import TemplateData from "../src/Data/TemplateData.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { isTypeScriptSupported } from "../src/Util/FeatureTests.cjs"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; const pkg = createRequire(import.meta.url)("../package.json"); async function testGetLocalData(tmplData, templatePath) { let localDataPaths = await tmplData.getLocalDataPaths(templatePath); let importedData = await tmplData.combineLocalData(localDataPaths); let globalData = await tmplData.getGlobalData(); // OK-ish: shallow merge when combining template/data dir files with global data files let localData = Object.assign({}, globalData, importedData); // debug("`getLocalData` for %o: %O", templatePath, localData); return localData; } test("Create", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }) let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.true(Object.keys(data[eleventyConfig.getConfig().keys.package]).length > 0); }); test("getGlobalData()", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let config = eleventyConfig.getConfig(); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); t.is(dataObj.getGlobalData().toString(), "[object Promise]"); let data = await dataObj.getGlobalData(); t.is(data.globalData.datakey1, "datavalue1", "simple data value"); t.is( data.globalData.datakey2, "{{pkg.name}}", `variables, resolve ${config.keys.package} to its value.` ); t.true( Object.keys(data[config.keys.package]).length > 0, `package.json imported to data in ${config.keys.package}` ); }); test("getGlobalData() use default processing (false)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.globalData.datakey2, "{{pkg.name}}", `variables should not resolve`); }); test("Data dir does not exist", async (t) => { await t.throwsAsync(async () => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/thisdirectorydoesnotexist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); await dataObj.getGlobalData(); }, { message: "The \"test/thisdirectorydoesnotexist\" `input` parameter (directory or file path) must exist on the file system (unless detected as a glob by the `tinyglobby` package)" }); }); test("Add local data", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.globalData.datakey1, "datavalue1"); t.is(data.globalData.datakey2, "{{pkg.name}}"); let withLocalData = await testGetLocalData(dataObj, "./test/stubs/component/component.njk"); t.is(withLocalData.globalData.datakey1, "datavalue1"); t.is(withLocalData.globalData.datakey2, "{{pkg.name}}"); t.is(withLocalData.localdatakey1, "localdatavalue1"); // from the js file // this checks priority/overrides t.is(withLocalData.localdatakeyfromcjs, "common-js-howdydoody"); t.is(withLocalData.localdatakeyfromjs, "howdydoody"); t.is(withLocalData.localdatakeyfromjs2, "howdy2"); }); test("Get local data async JS", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let withLocalData = await testGetLocalData(dataObj, "./test/stubs/component-async/component.njk"); // from the js file t.is(withLocalData.localdatakeyfromjs, "howdydoody"); t.is(withLocalData.localdatakeyfromcjs, "common-js-howdydoody"); }); test("addLocalData() doesn’t exist but doesn’t fail (template file does exist)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let beforeDataKeyCount = Object.keys(data); // template file does exist let withLocalData = await testGetLocalData( dataObj, "./test/stubs/datafiledoesnotexist/template.njk" ); t.is(withLocalData.globalData.datakey1, "datavalue1"); t.is(withLocalData.globalData.datakey2, "{{pkg.name}}"); t.deepEqual(Object.keys(withLocalData), beforeDataKeyCount); }); test("addLocalData() doesn’t exist but doesn’t fail (template file does not exist)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let beforeDataKeyCount = Object.keys(data); let withLocalData = await testGetLocalData( dataObj, "./test/stubs/datafiledoesnotexist/templatedoesnotexist.njk" ); t.is(withLocalData.globalData.datakey1, "datavalue1"); t.is(withLocalData.globalData.datakey2, "{{pkg.name}}"); t.deepEqual(Object.keys(withLocalData), beforeDataKeyCount); }); test("Global Dir Directory", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "." } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); t.deepEqual(dataObj.getGlobalDataGlob(), ["./_data/**/*.{json,mjs,cjs,js}"]); }); test("Global Dir Directory with Constructor Path Arg", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); t.deepEqual(dataObj.getGlobalDataGlob(), ["./test/stubs/_data/**/*.{json,mjs,cjs,js}"]); }); test("getAllGlobalData() with other data files", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let dataFilePaths = await dataObj.getGlobalDataFiles(); t.true(dataFilePaths.length > 0); t.true( dataFilePaths.filter((path) => { return path.indexOf("./test/stubs/_data/globalData.json") === 0; }).length > 0 ); t.truthy(data.globalData); t.is(data.globalData.datakey1, "datavalue1"); t.truthy(data.testData); t.deepEqual(data.testData, { testdatakey1: "testdatavalue1", }); t.deepEqual(data.subdir.testDataSubdir, { subdirkey: "subdirvalue", }); }); test("getAllGlobalData() with js object data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let dataFilePaths = await dataObj.getGlobalDataFiles(); t.true( dataFilePaths.filter((path) => { return path.indexOf("./test/stubs/_data/globalData2.cjs") === 0; }).length > 0 ); t.truthy(data.globalData2); t.is(data.globalData2.datakeyfromjs, "howdy"); }); test("getAllGlobalData() with js function data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let dataFilePaths = await dataObj.getGlobalDataFiles(); t.true( dataFilePaths.filter((path) => { return path.indexOf("./test/stubs/_data/globalDataFn.js") === 0; }).length > 0 ); t.truthy(data.globalDataFn); t.is(data.globalDataFn.datakeyfromjsfn, "howdy"); }); test("getAllGlobalData() with config globalData", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.addGlobalData("example", () => "one"); cfg.addGlobalData("example2", async () => "two"); cfg.addGlobalData("example3", "static"); }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.example, "one"); t.is(data.example2, "two"); t.is(data.example3, "static"); }); test("getAllGlobalData() with common js function data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let dataFilePaths = await dataObj.getGlobalDataFiles(); t.true( dataFilePaths.filter((path) => { return path.indexOf("./test/stubs/_data/globalDataFnCJS.cjs") === 0; }).length > 0 ); t.truthy(data.globalDataFnCJS); t.is(data.globalDataFnCJS.datakeyfromcjsfn, "common-cjs-howdy"); }); test("getDataValue() without template engine preprocessing", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); let data = await dataObj.getDataValue("./test/stubs/_data/testDataLiquid.json", { pkg: { name: "pkgname" }, }); t.deepEqual(data, { datakey1: "datavalue1", datakey2: "{{ pkg.name }}", }); }); test("getLocalDataPaths", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths (with setDataFileBaseName #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileBaseName("index"); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/index.11tydata.json", "./test/stubs/index.11tydata.mjs", "./test/stubs/index.11tydata.cjs", "./test/stubs/index.11tydata.js", "./test/stubs/component/index.11tydata.json", "./test/stubs/component/index.11tydata.mjs", "./test/stubs/component/index.11tydata.cjs", "./test/stubs/component/index.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths (with empty setDataFileSuffixes #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileSuffixes([]); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, []); }); test("getLocalDataPaths (with setDataFileSuffixes override #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileSuffixes([".howdy"]); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.howdy.json", "./test/stubs/stubs.howdy.mjs", "./test/stubs/stubs.howdy.cjs", "./test/stubs/stubs.howdy.js", "./test/stubs/component/component.howdy.json", "./test/stubs/component/component.howdy.mjs", "./test/stubs/component/component.howdy.cjs", "./test/stubs/component/component.howdy.js", ]); }); test("getLocalDataPaths (with setDataFileSuffixes empty string override #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileSuffixes([""]); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, ["./test/stubs/stubs.json", "./test/stubs/component/component.json"]); }); test("getLocalDataPaths (with setDataFileSuffixes override with two entries #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileSuffixes([".howdy", ""]); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.howdy.json", "./test/stubs/stubs.howdy.mjs", "./test/stubs/stubs.howdy.cjs", "./test/stubs/stubs.howdy.js", "./test/stubs/component/component.json", "./test/stubs/component/component.howdy.json", "./test/stubs/component/component.howdy.mjs", "./test/stubs/component/component.howdy.cjs", "./test/stubs/component/component.howdy.js", ]); }); test("getLocalDataPaths (with setDataFileSuffixes and setDataFileBaseName #1699)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.setDataFileBaseName("index"); cfg.setDataFileSuffixes([".howdy", ""]); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/index.howdy.json", "./test/stubs/index.howdy.mjs", "./test/stubs/index.howdy.cjs", "./test/stubs/index.howdy.js", "./test/stubs/component/index.howdy.json", "./test/stubs/component/index.howdy.mjs", "./test/stubs/component/index.howdy.cjs", "./test/stubs/component/index.howdy.js", "./test/stubs/component/component.json", "./test/stubs/component/component.howdy.json", "./test/stubs/component/component.howdy.mjs", "./test/stubs/component/component.howdy.cjs", "./test/stubs/component/component.howdy.js", ]); }); test("Deeper getLocalDataPaths", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/test.json", "./test/test.11tydata.json", "./test/test.11tydata.mjs", "./test/test.11tydata.cjs", "./test/test.11tydata.js", "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths with an 11ty js template", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.11ty.js"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths with inputDir passed in (trailing slash)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths with inputDir passed in (no trailing slash)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getLocalDataPaths with inputDir passed in (no leading slash)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let paths = await dataObj.getLocalDataPaths("./test/stubs/component/component.liquid"); t.deepEqual(paths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); }); test("getRawImports", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); let data = await dataObj.getRawImports(); t.is(data.pkg.name, "@11ty/eleventy"); }); test("getTemplateDataFileGlob", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs" } }); let tw = new TemplateData(eleventyConfig); t.deepEqual(await tw.getTemplateDataFileGlob(), [ `./test/stubs/**/*.{json,11tydata.mjs,11tydata.cjs,11tydata.js${isTypeScriptSupported() ? ",11tydata.mts,11tydata.cts,11tydata.ts" : ""}}`, ]); }); // https://github.com/11ty/eleventy/issues/3937 test("TemplateData.merge is now Merge()", (t) => { t.deepEqual( Merge( { tags: [1, 2, 3], }, { tags: [4, 5, 6], } ), { tags: [1, 2, 3, 4, 5, 6] } ); }); test("TemplateData.cleanupData", (t) => { t.deepEqual(TemplateData.cleanupData({}), {}); t.deepEqual(TemplateData.cleanupData({ tags: null }), { tags: [] }); t.deepEqual(TemplateData.cleanupData({ tags: "" }), { tags: [] }); t.deepEqual(TemplateData.cleanupData({ tags: [] }), { tags: [] }); t.deepEqual(TemplateData.cleanupData({ tags: "test" }), { tags: ["test"] }); t.deepEqual(TemplateData.cleanupData({ tags: ["test1", "test2"] }), { tags: ["test1", "test2"], }); }); test("Parent directory for data (Issue #337)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs-337/src/", data: "../data/", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.deepEqual(data.xyz, { hi: "bye", }); }); test("Dots in datafile path (Issue #1242)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs-1242/", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.deepEqual(data["xyz.dottest"], { hi: "bye", test: { abc: 42, }, }); }); test("addGlobalData values", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "./test/stubs-global-data-config-api/" }, function(cfg) { cfg.addGlobalData("myFunction", () => "fn-value"); cfg.addGlobalData("myPromise", () => { return new Promise((resolve) => { setTimeout(resolve, 100, "promise-value"); }); }); cfg.addGlobalData("myAsync", async () => Promise.resolve("promise-value")); }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.myFunction, "fn-value"); t.is(data.myPromise, "promise-value"); t.is(data.myAsync, "promise-value"); }); test("addGlobalData should execute once.", async (t) => { let count = 0; let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "./test/stubs-global-data-config-api/" }, function(cfg) { cfg.addGlobalData("count", () => { count++; return count; }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.count, 1); t.is(count, 1); }); test("addGlobalData complex key", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "./test/stubs-global-data-config-api-nested/" }, function(cfg) { cfg.addGlobalData("deep.nested.one", () => "first"); cfg.addGlobalData("deep.nested.two", () => "second"); }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.deep.existing, true); t.is(data.deep.nested.one, "first"); t.is(data.deep.nested.two, "second"); }); test("eleventy.version and eleventy.generator returned from data", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "./test/stubs-empty/" }, function(cfg) { cfg.addGlobalData("deep.nested.one", () => "first"); cfg.addGlobalData("deep.nested.two", () => "second"); }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); let version = semver.coerce(pkg.version).toString(); t.is(data.eleventy.version, version); t.is(data.eleventy.generator, `Eleventy v${version}`); t.is(data.deep.nested.one, "first"); t.is(data.deep.nested.two, "second"); }); test("getGlobalData() empty json file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs-empty-json-data/", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.deepEqual(data.empty, {}); }); test("ESM data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs-data-esm/", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.module.default, "es module default"); t.is(data.module.named, "es module named"); t.is(data.commonjs, "commonjs default"); }); test("Test collection names from data (empty assigned)", async (t) => { t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: [], }), ["all"]); t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: [], eleventyExcludeFromCollections: true }), []); t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: [], eleventyExcludeFromCollections: ["one"] }), ["all"]); }); test("Test collection names from data (tags assigned)", async (t) => { t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: ["one", "two"], }), ["all", "one", "two"]); t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: ["one", "two"], eleventyExcludeFromCollections: true, }), []); t.deepEqual(TemplateData.getIncludedCollectionNames({ tags: ["one", "two"], eleventyExcludeFromCollections: ["one"], }), ["all", "two"]); }); test("Test tag names from data (empty assigned)", async (t) => { t.deepEqual(TemplateData.getIncludedTagNames({ tags: [], }), []); t.deepEqual(TemplateData.getIncludedTagNames({ tags: [], eleventyExcludeFromCollections: true }), []); t.deepEqual(TemplateData.getIncludedTagNames({ tags: [], eleventyExcludeFromCollections: ["one"] }), []); }); test("Test tag names from data (tags assigned)", async (t) => { t.deepEqual(TemplateData.getIncludedTagNames({ tags: ["one", "two"], }), ["one", "two"]); t.deepEqual(TemplateData.getIncludedTagNames({ tags: ["one", "two"], eleventyExcludeFromCollections: true, }), []); t.deepEqual(TemplateData.getIncludedTagNames({ tags: ["one", "two"], eleventyExcludeFromCollections: ["one"], }), ["two"]); }); ================================================ FILE: test/TemplateDepGraphTest.js ================================================ import test from "ava"; import { TemplateDepGraph } from "../src/Util/TemplateDepGraph.js"; test("Using new Template DepGraph", async (t) => { let graph = new TemplateDepGraph(); graph.addTemplate("template-paginated-over-all.njk", ["all"], []); graph.addTemplate("template-paginated-over-userconfig.njk", ["[userconfig]"], []); graph.addTemplate("template-1.njk", [], ["all", "posts"]); graph.addTemplate("template-2.njk", [], ["all", "posts", "dog"]); graph.addTemplate("template-paginated-collections.njk", ["[keys]"], []); graph.addConfigCollectionName("myCollection"); t.deepEqual(graph.unfilteredOrder(), [ "template-1.njk", "template-2.njk", "__collection:posts", "__collection:dog", "__collection:[basic]", "__collection:[userconfig]", "template-paginated-over-userconfig.njk", "__collection:myCollection", "__collection:[keys]", "template-paginated-collections.njk", "__collection:all", "template-paginated-over-all.njk", ]); t.deepEqual(graph.overallOrder(), [ "template-1.njk", "template-2.njk", "__collection:posts", "__collection:dog", "template-paginated-over-userconfig.njk", "__collection:myCollection", "__collection:[keys]", "template-paginated-collections.njk", "__collection:all", "template-paginated-over-all.njk", "__collection:all", ]); }); ================================================ FILE: test/TemplateEngineManagerTest.js ================================================ import test from "ava"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateConfig from "../src/TemplateConfig.js"; test("Unsupported engine", async (t) => { await t.throwsAsync(async () => { let eleventyConfig = new TemplateConfig(); let tem = new TemplateEngineManager(eleventyConfig); await tem.getEngine("doesnotexist"); }); }); test("Supported engine", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let tem = new TemplateEngineManager(eleventyConfig); t.truthy(tem.hasEngine("11ty.js")); }); test("Supported custom engine", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.extensionMap.add({ extension: "txt", key: "txt", compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); await eleventyConfig.init(); let extensionMap = new EleventyExtensionMap(eleventyConfig); let tem = new TemplateEngineManager(eleventyConfig); t.truthy(tem.hasEngine("txt")); let engine = await tem.getEngine("txt", extensionMap); let fn = await engine.compile("

This is plaintext

"); t.is(await fn({ author: "zach" }), "

This is plaintext

"); }); test("Custom engine with custom init", async (t) => { let initCount = 0; let compileCount = 0; let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.extensionMap.add({ extension: "custom1", key: "custom1", init: async function () { // do custom things, but only once initCount++; }, compile: function (str, inputPath) { compileCount++; return () => str; }, }); await eleventyConfig.init(); let extensionMap = new EleventyExtensionMap(eleventyConfig); // let config = eleventyConfig.getConfig(); let tem = new TemplateEngineManager(eleventyConfig); t.truthy(tem.hasEngine("custom1")); let engine = await tem.getEngine("custom1", extensionMap); let fn = await engine.compile("

This is plaintext

"); t.is(await fn({}), "

This is plaintext

"); let engine2 = await tem.getEngine("custom1"); t.is(engine, engine2); let fn2 = await engine2.compile("

This is plaintext

"); t.is(await fn2({}), "

This is plaintext

"); t.is(initCount, 1, "Should have only run the init callback once"); t.is(compileCount, 2, "Should have only run the compile callback twice"); }); test("getEngineLib", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let extensionMap = new EleventyExtensionMap(eleventyConfig); let tem = new TemplateEngineManager(eleventyConfig); let engine = await tem.getEngine("md", extensionMap); t.truthy(engine.getEngineLib()); }); ================================================ FILE: test/TemplateEngineTest.js ================================================ import test from "ava"; import TemplateEngine from "../src/Engines/TemplateEngine.js"; import { getTemplateConfigInstance } from "./_testHelpers.js" test("Unsupported engine", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let engine = new TemplateEngine("doesnotexist", eleventyConfig); t.is(engine.getName(), "doesnotexist"); }); test("Supported engine", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); t.is(new TemplateEngine("liquid", eleventyConfig).getName(), "liquid"); }); ================================================ FILE: test/TemplateFileSlugTest.js ================================================ import test from "ava"; import TemplateFileSlug from "../src/TemplateFileSlug.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getNewSlugInstance(path, inputDir) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); let extensionMap = new EleventyExtensionMap(eleventyConfig); extensionMap.setFormats([]); let fs = new TemplateFileSlug(path, extensionMap, eleventyConfig); return fs; } test("Easy slug", async (t) => { let fs = await getNewSlugInstance("./file.html"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with dot", async (t) => { let fs = await getNewSlugInstance("./file.test.html"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug with dot 11ty.js", async (t) => { let fs = await getNewSlugInstance("./file.test.11ty.js"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug with date", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-file.html"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with date and dot in slug", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-file.test.html"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug, index", async (t) => { let fs = await getNewSlugInstance("./index.html"); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); test("Easy slug with date, index", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-index.html"); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); test("Easy slug with only a date and no suffix", async (t) => { let fs = await getNewSlugInstance("./2018-01-01.html"); t.is(fs.getSlug(), "2018-01-01"); t.is(fs.getFullPathWithoutExtension(), "/2018-01-01"); }); /* Directories */ test("Easy slug with dir", async (t) => { let fs = await getNewSlugInstance("./test/file.html"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/test/file"); }); test("Easy slug with dot with dir", async (t) => { let fs = await getNewSlugInstance("./test/file.test.html"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/test/file.test"); }); test("Easy slug with date with dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-file.html"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/test/file"); }); test("Easy slug with date and dot in slug with dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-file.test.html"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/test/file.test"); }); test("Easy slug, index with dir", async (t) => { let fs = await getNewSlugInstance("./test/index.html"); t.is(fs.getSlug(), "test"); t.is(fs.getFullPathWithoutExtension(), "/test/index"); }); test("Easy slug with date, index with dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-index.html"); t.is(fs.getSlug(), "test"); t.is(fs.getFullPathWithoutExtension(), "/test/index"); }); test("Strips date from dir name", async (t) => { let fs = await getNewSlugInstance("./2021-11-20-my-awesome-post/index.md"); t.is(fs.getSlug(), "my-awesome-post"); t.is(fs.getFullPathWithoutExtension(), "/2021-11-20-my-awesome-post/index"); }); /* Pass Input dir */ test("Easy slug, input dir", async (t) => { let fs = await getNewSlugInstance("./file.html", "."); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with dot, input dir", async (t) => { let fs = await getNewSlugInstance("./file.test.html", "."); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug with date, input dir", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-file.html", "."); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with date and dot in slug, input dir", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-file.test.html", "."); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug, index, input dir", async (t) => { let fs = await getNewSlugInstance("./index.html", "."); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); test("Easy slug with date, index, input dir", async (t) => { let fs = await getNewSlugInstance("./2018-01-01-index.html", "."); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); /* Directories and Input Dir */ test("Easy slug with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/file.html", "./test"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with dot with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/file.test.html", "./test"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug with date with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-file.html", "./test"); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/file"); }); test("Easy slug with date and dot in slug with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-file.test.html", "./test"); t.is(fs.getSlug(), "file.test"); t.is(fs.getFullPathWithoutExtension(), "/file.test"); }); test("Easy slug, index with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/index.html", "./test"); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); test("Easy slug with date, index with dir and input dir", async (t) => { let fs = await getNewSlugInstance("./test/2018-01-01-index.html", "./test"); t.is(fs.getSlug(), ""); t.is(fs.getFullPathWithoutExtension(), "/index"); }); test("Easy slug with multiple dirs", async (t) => { let fs = await getNewSlugInstance("./dir1/dir2/dir3/file.html", "."); t.is(fs.getSlug(), "file"); t.is(fs.getFullPathWithoutExtension(), "/dir1/dir2/dir3/file"); }); test("Easy slug with multiple dirs and an index file", async (t) => { let fs = await getNewSlugInstance("./dir1/dir2/dir3/index.html", "."); t.is(fs.getSlug(), "dir3"); t.is(fs.getFullPathWithoutExtension(), "/dir1/dir2/dir3/index"); }); ================================================ FILE: test/TemplateGlobTest.js ================================================ import test from "ava"; import { glob } from 'tinyglobby'; import { TemplatePath } from "@11ty/eleventy-utils"; import TemplateGlob from "../src/TemplateGlob.js"; test("TemplatePath assumptions", (t) => { t.is(TemplatePath.normalize("ignoredFolder"), "ignoredFolder"); t.is(TemplatePath.normalize("./ignoredFolder"), "ignoredFolder"); t.is(TemplatePath.normalize("./ignoredFolder/"), "ignoredFolder"); }); test("Normalize string argument", (t) => { t.deepEqual(TemplateGlob.map("views"), "./views"); t.deepEqual(TemplateGlob.map("views/"), "./views"); t.deepEqual(TemplateGlob.map("./views"), "./views"); t.deepEqual(TemplateGlob.map("./views/"), "./views"); }); test("Normalize with nots", (t) => { t.deepEqual(TemplateGlob.map("!views"), "!./views"); t.deepEqual(TemplateGlob.map("!views/"), "!./views"); t.deepEqual(TemplateGlob.map("!./views"), "!./views"); t.deepEqual(TemplateGlob.map("!./views/"), "!./views"); }); test("Normalize with globstar", (t) => { t.deepEqual(TemplateGlob.map("!views/**"), "!./views/**"); t.deepEqual(TemplateGlob.map("!./views/**"), "!./views/**"); }); test("Normalize with globstar and star", (t) => { t.deepEqual(TemplateGlob.map("!views/**/*"), "!./views/**/*"); t.deepEqual(TemplateGlob.map("!./views/**/*"), "!./views/**/*"); }); test("Normalize with globstar and star and file extension", (t) => { t.deepEqual(TemplateGlob.map("!views/**/*.json"), "!./views/**/*.json"); t.deepEqual(TemplateGlob.map("!./views/**/*.json"), "!./views/**/*.json"); }); test("NormalizePath with globstar and star and file extension", (t) => { t.deepEqual(TemplateGlob.normalizePath("views", "/", "**/*.json"), "./views/**/*.json"); t.deepEqual(TemplateGlob.normalizePath("./views", "/", "**/*.json"), "./views/**/*.json"); }); test("NormalizePath with globstar and star and file extension (errors)", (t) => { t.throws(() => { TemplateGlob.normalizePath("!views/**/*.json"); }); t.throws(() => { TemplateGlob.normalizePath("!views", "/", "**/*.json"); }); t.throws(() => { TemplateGlob.normalizePath("!./views/**/*.json"); }); t.throws(() => { TemplateGlob.normalizePath("!./views", "/", "**/*.json"); }); }); test("Normalize array argument", (t) => { t.deepEqual(TemplateGlob.map(["views", "content"]), ["./views", "./content"]); t.deepEqual(TemplateGlob.map("views/"), "./views"); t.deepEqual(TemplateGlob.map("./views"), "./views"); t.deepEqual(TemplateGlob.map("./views/"), "./views"); }); test("matuzo project issue with fastglob assumptions", async (t) => { let dotslashincludes = await glob( TemplateGlob.map([ "./test/stubs/globby/**/*.html", "!./test/stubs/globby/_includes/**/*", "!./test/stubs/globby/_data/**/*", ]) ); t.is( dotslashincludes.filter(function (file) { return file.indexOf("_includes") > -1; }).length, 0 ); let globincludes = await glob( TemplateGlob.map([ "test/stubs/globby/**/*.html", "!./test/stubs/globby/_includes/**/*", "!./test/stubs/globby/_data/**/*", ]) ); t.is( globincludes.filter(function (file) { return file.indexOf("_includes") > -1; }).length, 0 ); }); // `fast-glob` isn't used any more, but the test can stay as a sanity check. test("fastglob assumptions", async (t) => { let globbed = await glob("test/stubs/ignoredFolder/**"); t.is(globbed.length, 1); let globbed2 = await glob("test/stubs/ignoredFolder/**/*"); t.is(globbed2.length, 1); let globbed3 = await glob([ "./test/stubs/ignoredFolder/**/*.md", "!./test/stubs/ignoredFolder/**", ]); t.is(globbed3.length, 0); let globbed4 = await glob(["./test/stubs/ignoredFolder/*.md", "!./test/stubs/ignoredFolder/**"]); t.is(globbed4.length, 0); let globbed5 = await glob([ "./test/stubs/ignoredFolder/ignored.md", "!./test/stubs/ignoredFolder/**", ]); t.is(globbed5.length, 0); }); ================================================ FILE: test/TemplateLayoutPathResolverTest.js ================================================ import test from "ava"; import TemplateLayoutPathResolver from "../src/TemplateLayoutPathResolver.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; async function getResolverInstance(path, inputDir, { eleventyConfig, map } = {}) { if (!eleventyConfig) { eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); } if (!map) { map = new EleventyExtensionMap(eleventyConfig); map.setFormats(["liquid", "md", "njk", "html", "11ty.js"]); } return new TemplateLayoutPathResolver(path, map, eleventyConfig); } test("Layout", async (t) => { let res = await getResolverInstance("default", "./test/stubs"); t.is(res.getFileName(), "default.liquid"); }); test("Layout already has extension", async (t) => { let res = await getResolverInstance("default.liquid", "./test/stubs"); t.is(res.getFileName(), "default.liquid"); }); test("Layout (uses empty string includes folder)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", includes: "" } }); let res = await getResolverInstance("includesemptystring", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "includesemptystring.liquid"); }); test("Layout (uses empty string includes folder) already has extension", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", includes: "" } }); let res = await getResolverInstance("includesemptystring.liquid", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "includesemptystring.liquid"); }); test("Layout (uses layouts folder)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", layouts: "_layouts", includes: "_includes", } }); let res = await getResolverInstance("layoutsdefault", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "layoutsdefault.liquid"); }); test("Layout (uses layouts folder) already has extension", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", layouts: "_layouts", } }); let res = await getResolverInstance("layoutsdefault.liquid", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "layoutsdefault.liquid"); }); test("Layout (uses empty string layouts folder)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", layouts: "", } }); let res = await getResolverInstance("layoutsemptystring", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "layoutsemptystring.liquid"); }); test("Layout (uses empty string layouts folder) no template resolution", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", layouts: "" }, function(cfg) { cfg.setLayoutResolution(false); }); let res = await getResolverInstance("layoutsemptystring", "./test/stubs", { eleventyConfig, }); t.throws(() => { res.getFileName(); }, { message: `You’re trying to use a layout that does not exist: test/stubs/layoutsemptystring (via \`layout: layoutsemptystring\`)` }); }); test("Layout (uses empty string layouts folder) already has extension", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ templateFormats: ["liquid"], dir: { input: "test/stubs", layouts: "", } }); let res = await getResolverInstance("layoutsemptystring.liquid", "./test/stubs", { eleventyConfig, }); t.is(res.getFileName(), "layoutsemptystring.liquid"); }); test("Layout subdir", async (t) => { let res = await getResolverInstance("layouts/inasubdir", "./test/stubs"); t.is(res.getFileName(), "layouts/inasubdir.njk"); }); test("Layout subdir already has extension", async (t) => { let res = await getResolverInstance("layouts/inasubdir.njk", "./test/stubs"); t.is(res.getFileName(), "layouts/inasubdir.njk"); }); test("Multiple layouts exist with the same file base, pick one", async (t) => { let res = await getResolverInstance("multiple", "./test/stubs"); // pick the first one if multiple exist. t.is(res.getFileName(), "multiple.liquid"); }); test("Multiple layouts exist but we are being explicit—layout already has extension", async (t) => { let res = await getResolverInstance("multiple.liquid", "./test/stubs"); t.is(res.getFileName(), "multiple.liquid"); let res2 = await getResolverInstance("multiple.md", "./test/stubs"); t.is(res2.getFileName(), "multiple.md"); }); test("Layout is aliased to a new location", async (t) => { let tl = await getResolverInstance("post", "./test/stubs"); tl.addLayoutAlias("post", "layouts/post.liquid"); tl.init(); t.is(tl.getFileName(), "layouts/post.liquid"); }); test("Global default with empty string alias", async (t) => { let tl = await getResolverInstance("", "./test/stubs"); tl.addLayoutAlias("", "layouts/post.liquid"); tl.init(); t.is(tl.getFileName(), "layouts/post.liquid"); }); test("Global default with empty string alias (but no alias exists for this instance)", async (t) => { let tl = await getResolverInstance("layout.liquid", "./test/stubs"); tl.addLayoutAlias("", "layouts/post.liquid"); tl.init(); t.throws(() => { tl.getFileName(); }); }); test("Layout has no alias and does not exist", async (t) => { let tl = await getResolverInstance("shouldnotexist", "./test/stubs"); tl.addLayoutAlias("post", "layouts/post.liquid"); tl.init(); t.throws(() => { tl.getFileName(); }); t.throws(() => { tl.getFullPath(); }); }); ================================================ FILE: test/TemplateLayoutTest.js ================================================ import test from "ava"; import TemplateLayout from "../src/TemplateLayout.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { renderLayoutViaLayout } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getTemplateLayoutInstance(key, inputDir, map) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir, } }); let mgr = new TemplateEngineManager(eleventyConfig); if (!map) { map = new EleventyExtensionMap(eleventyConfig); map.setFormats(["liquid", "md", "njk", "html", "11ty.js"]); map.engineManager = mgr; } let layout = new TemplateLayout(key, map, eleventyConfig); return layout; } test("Creation", async (t) => { let tl = await getTemplateLayoutInstance("base", "./test/stubs"); t.is(tl.getInputPath(), "./test/stubs/_includes/base.njk"); await t.throwsAsync(async () => { await getTemplateLayoutInstance("doesnotexist", "./test/stubs"); }); }); test("Get Layout Chain", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-inherit-a.njk", "./test/stubs"); await tl.getData(); t.deepEqual(await tl.getLayoutChain(), [ "./test/stubs/_includes/layouts/layout-inherit-a.njk", "./test/stubs/_includes/layouts/layout-inherit-b.njk", "./test/stubs/_includes/layouts/layout-inherit-c.njk", ]); }); test("Get Front Matter Data", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-inherit-a.njk", "./test/stubs"); t.is(tl.getInputPath(), "./test/stubs/_includes/layouts/layout-inherit-a.njk"); let data = await tl.getData(); t.deepEqual(data, { inherits: "a", secondinherits: "b", thirdinherits: "c", }); t.deepEqual(await tl.getLayoutChain(), [ "./test/stubs/_includes/layouts/layout-inherit-a.njk", "./test/stubs/_includes/layouts/layout-inherit-b.njk", "./test/stubs/_includes/layouts/layout-inherit-c.njk", ]); t.deepEqual(await tl.getData(), { inherits: "a", secondinherits: "b", thirdinherits: "c", }); }); test("Render Layout", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-inherit-a.njk", "./test/stubs"); t.is( ( await renderLayoutViaLayout(tl, { inherits: "a", secondinherits: "b", thirdinherits: "c", }) ).trim(), "a b a c" ); }); test("Render Layout (Pass in template content)", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-inherit-a.njk", "./test/stubs"); t.is( ( await renderLayoutViaLayout(tl, { inherits: "a", secondinherits: "b", thirdinherits: "c" }, "TEMPLATE_CONTENT" ) ).trim(), "TEMPLATE_CONTENT a b a c" ); }); test("Render Layout (Pass in undefined template content)", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-contentdump.njk", "./test/stubs"); t.is( await renderLayoutViaLayout(tl, { inherits: "a", secondinherits: "b", thirdinherits: "c" }, undefined), "this is bad a b a c" ); }); test("Render Layout (Pass in null template content)", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-contentdump.njk", "./test/stubs"); t.is( await renderLayoutViaLayout(tl, { inherits: "a", secondinherits: "b", thirdinherits: "c" }, null), " a b a c" ); }); test("Render Layout (Pass in empty template content)", async (t) => { let tl = await getTemplateLayoutInstance("layouts/layout-contentdump.njk", "./test/stubs"); t.is(await renderLayoutViaLayout(tl, { inherits: "a", secondinherits: "b", thirdinherits: "c" }, ""), " a b a c"); }); test("Cache Duplicates (use full key for cache)", async (t) => { // if two different layouts used the same filename but had different inputdirs, make sure templatelayout cache is unique let tla = await getTemplateLayoutInstance( "layout.njk", "./test/stubs/templateLayoutCacheDuplicates" ); t.is((await renderLayoutViaLayout(tla, {})).trim(), "Hello A"); let tlb = await getTemplateLayoutInstance( "layout.njk", "./test/stubs/templateLayoutCacheDuplicates-b" ); t.is((await renderLayoutViaLayout(tlb, {})).trim(), "Hello B"); t.is((await renderLayoutViaLayout(tla, {})).trim(), "Hello A"); }); test("Throw an error if a layout references itself as the layout", async (t) => { await t.throwsAsync(async () => { const tl = await getTemplateLayoutInstance( "layout-cycle-self.njk", "./test/stubs-circular-layout" ); const layoutChain = await tl._testGetLayoutChain(); return layoutChain; }); }); test("Throw an error if a circular layout chain is detected", async (t) => { await t.throwsAsync(async () => { const tl = await getTemplateLayoutInstance( "layout-cycle-a.njk", "./test/stubsstubs-circular-layout" ); const layoutChain = await tl._testGetLayoutChain(); return layoutChain; }); }); ================================================ FILE: test/TemplateMapTest-ComputedData.js ================================================ import test from "ava"; import TemplateData from "../src/Data/TemplateData.js"; import TemplateMap from "../src/TemplateMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; test("Computed data can see tag generated collections", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-computed-collections" } }); let tm = new TemplateMap(eleventyConfig); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-computed-collections/collections.njk", "./test/stubs-computed-collections/", "./dist", dataObj, null, eleventyConfig ); await tm.add(tmpl); let dataObj2 = new TemplateData(eleventyConfig); dataObj2.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl2 = await getNewTemplate( "./test/stubs-computed-collections/dog.njk", "./test/stubs-computed-collections/", "./dist", dataObj2, null, eleventyConfig ); await tm.add(tmpl2); await tm.cache(); let map = tm.getMap(); t.is(map[0].inputPath.endsWith("collections.njk"), true); t.truthy(map[0].data.collections.all); t.is(map[0].data.collections.all.length, 2); t.truthy(map[0].data.collections.dog); t.is(map[0].data.collections.dog.length, 1); t.truthy(map[0].data.dogCollection); t.is(map[0].data.dogCollection.length, 1); t.is(map[0].data.test, "hello"); // THEY ARE THE SAME t.is(map[0].data.dogCollection, map[0].data.collections.dog); }); test("Computed data can see paginated data, Issue #1138", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-computed-pagination" } }); let tm = new TemplateMap(eleventyConfig); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-computed-pagination/paginated.njk", "./test/stubs-computed-pagination/", "./dist", dataObj, null, eleventyConfig ); await tm.add(tmpl); let dataObj2 = new TemplateData(eleventyConfig); dataObj2.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl2 = await getNewTemplate( "./test/stubs-computed-pagination/child.11ty.cjs", "./test/stubs-computed-pagination/", "./dist", dataObj2, null, eleventyConfig ); await tm.add(tmpl2); await tm.cache(); let map = tm.getMap(); t.is(map.length, 2); // paginated template tests t.is(map[0].inputPath.endsWith("paginated.njk"), true); t.is(map[0]._pages.length, 2); t.is(map[0]._pages[0].data.venue, "first"); t.is(map[0]._pages[0].data.title, "first"); t.is(map[0]._pages[0].url, "/venues/first/"); t.truthy(map[0]._pages[0].data.collections); t.truthy(map[0]._pages[0].data.collections.venue); t.is(map[0]._pages[0].data.collections.venue.length, 2); t.is(map[0]._pages[1].data.venue, "second"); t.is(map[0]._pages[1].data.title, "second"); t.is(map[0]._pages[1].url, "/venues/second/"); t.truthy(map[0]._pages[1].data.collections.venue); t.is(map[0]._pages[1].data.collections.venue.length, 2); // consumer of paginated template tests t.is(map[1]._pages.length, 1); // computed prop from venues t.truthy(map[1]._pages[0].data.venues); t.is(map[1]._pages[0].data.venues.length, 2); }); test("Computed data in directory data file consumes data file data, Issue #1137", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-computed-dirdata" } }); let tm = new TemplateMap(eleventyConfig); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-computed-dirdata/dir/first.11ty.cjs", "./test/stubs-computed-dirdata/", "./dist", dataObj, null, eleventyConfig ); await tm.add(tmpl); let dataObj2 = new TemplateData(eleventyConfig); dataObj2.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl2 = await getNewTemplate( "./test/stubs-computed-dirdata/dir/second.11ty.cjs", "./test/stubs-computed-dirdata/", "./dist", dataObj2, null, eleventyConfig ); await tm.add(tmpl2); await tm.cache(); let map = tm.getMap(); t.is(map.length, 2); t.is(map[0]._pages[0].data.webmentions, "first"); t.is(map[1]._pages[0].data.webmentions, "second"); }); test("Computed data can filter collections (and other array methods)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-computed-collections-filter" } }); let tm = new TemplateMap(eleventyConfig); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-computed-collections-filter/collections.njk", "./test/stubs-computed-collections-filter/", "./dist", dataObj, null, eleventyConfig ); await tm.add(tmpl); let dataObj2 = new TemplateData(eleventyConfig); dataObj2.extensionMap = new EleventyExtensionMap(eleventyConfig); let tmpl2 = await getNewTemplate( "./test/stubs-computed-collections-filter/dog.njk", "./test/stubs-computed-collections-filter/", "./dist", dataObj2, null, eleventyConfig ); await tm.add(tmpl2); await tm.cache(); let map = tm.getMap(); t.is(map[0].inputPath.endsWith("collections.njk"), true); t.truthy(map[0].data.collections.all); t.is(map[0].data.collections.all.length, 2); t.truthy(map[0].data.collections.dog); t.is(map[0].data.collections.dog.length, 1); t.truthy(map[0].data.dogCollection); t.is(map[0].data.dogCollection.length, 1); t.is(map[0].data.test, "hello"); // Deeply equal but not the same per `filter` use. t.not(map[0].data.dogCollection, map[0].data.collections.dog); t.deepEqual(map[0].data.dogCollection, map[0].data.collections.dog); }); ================================================ FILE: test/TemplateMapTest.js ================================================ import test from "ava"; import TemplateMap from "../src/TemplateMap.js"; import TemplateCollection from "../src/TemplateCollection.js"; import UsingCircularTemplateContentReferenceError from "../src/Errors/UsingCircularTemplateContentReferenceError.js"; import TemplateContentUnrenderedTemplateError from "../src/Errors/TemplateContentUnrenderedTemplateError.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; import getNewTemplateForTests from "./_getNewTemplateForTests.js"; import { getRenderedTemplates as getRenderedTmpls, renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; function getNewTemplate(filename, input, output, eleventyConfig) { return getNewTemplateForTests(filename, input, output, null, null, eleventyConfig); } function getNewTemplateByNumber(num, eleventyConfig) { return getNewTemplate( `./test/stubs/templateMapCollection/test${num}.md`, "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); } async function testRenderWithoutLayouts(template, data) { let ret = await template.renderPageEntryWithoutLayout({ rawInput: await template.getPreRender(), data, }); return ret; } async function addTemplate(collection, template) { let data = await template.getData(); for (let map of await template.getTemplates(data)) { collection.add(map); } } test("TemplateMap has collections added", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await tm.cache(); t.is(tm.getMap().length, 2); t.is(tm.collection.getAll().length, 2); }); test("TemplateMap compared to Collection API", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl4); await tm.cache(); let map = tm.getMap(); t.deepEqual(map[0].template, tmpl1); t.deepEqual(map[0].data.collections.post[0].template, tmpl1); t.deepEqual(map[1].template, tmpl4); t.deepEqual(map[1].data.collections.post[1].template, tmpl4); let c = new TemplateCollection(); await addTemplate(c, tmpl1); await addTemplate(c, tmpl4); let posts = c.getFilteredByTag("post"); t.is(posts.length, 2); t.deepEqual(posts[0].template, tmpl1); t.deepEqual(posts[1].template, tmpl4); }); test("populating the collection twice should clear the previous values (--watch was making it cumulative)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await tm.cache(); await tm.cache(); t.is(tm.getMap().length, 2); }); test("TemplateMap adds collections data and has templateContent values", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let map = tm.getMap(); t.falsy(map[0].data.collections); t.falsy(map[1].data.collections); await tm.cache(); t.truthy(map[0]._pages[0].templateContent); t.truthy(map[1]._pages[0].templateContent); t.truthy(map[0].data.collections); t.truthy(map[1].data.collections); t.is(map[0].data.collections.post.length, 1); t.is(map[0].data.collections.all.length, 2); t.is(map[1].data.collections.post.length, 1); t.is(map[1].data.collections.all.length, 2); t.is( await testRenderWithoutLayouts(map[0].template, map[0].data), map[0]._pages[0].templateContent, ); t.is( await testRenderWithoutLayouts(map[1].template, map[1].data), map[1]._pages[0].templateContent, ); }); test("TemplateMap circular references (map without templateContent)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl3 = await getNewTemplateByNumber(3, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl3); let map = tm.getMap(); t.falsy(map[0].data.collections); await tm.cache(); t.truthy(map[0]._pages[0].templateContent); t.truthy(map[0].data.collections); t.is(map[0].data.collections.all.length, 1); t.is( await testRenderWithoutLayouts(map[0].template, map[0].data), map[0]._pages[0].templateContent, ); }); test("TemplateMap circular references (map.templateContent)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tm = new TemplateMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs/templateMapCollection/templateContent.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmpl); let map = tm.getMap(); t.falsy(map[0].data.collections); await t.throwsAsync( async () => { await tm.cache(); }, { instanceOf: UsingCircularTemplateContentReferenceError, }, ); }); test("Issue #115, mixing pagination and collections", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmplFoos = await getNewTemplate( "./test/stubs/issue-115/template-foos.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmplBars = await getNewTemplate( "./test/stubs/issue-115/template-bars.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmplIndex = await getNewTemplate( "./test/stubs/issue-115/index.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tm = new TemplateMap(eleventyConfig); await tm.add(tmplFoos); await tm.add(tmplBars); await tm.add(tmplIndex); await tm.cache(); let map = tm.getMap(); t.is(map.length, 3); t.deepEqual(map[2].template, tmplIndex); let collections = await tm._testGetAllCollectionsData(); t.is(Object.keys(collections.all).length, 3); t.is(Object.keys(collections.foos).length, 1); t.is(Object.keys(collections.bars).length, 1); t.is(Object.keys((await tm._testGetCollectionsData()).all).length, 3); t.is(Object.keys((await tm._testGetCollectionsData()).foos).length, 1); t.is(Object.keys((await tm._testGetCollectionsData()).bars).length, 1); t.truthy(map[0].data.collections); t.truthy(map[1].data.collections); t.truthy(map[2].data.collections); t.truthy(Object.keys(map[2].data.collections).length); t.is(Object.keys(map[0].data.collections.all).length, 3); t.is(Object.keys(map[0].data.collections.foos).length, 1); t.is(Object.keys(map[0].data.collections.bars).length, 1); t.is(Object.keys(map[1].data.collections.all).length, 3); t.is(Object.keys(map[1].data.collections.foos).length, 1); t.is(Object.keys(map[1].data.collections.bars).length, 1); t.is(Object.keys(map[2].data.collections.all).length, 3); t.is(Object.keys(map[2].data.collections.foos).length, 1); t.is(Object.keys(map[2].data.collections.bars).length, 1); let entry = await getRenderedTmpls(map[2].template, map[2].data); t.deepEqual( normalizeNewLines(entry[0].templateContent), `This page is foos This page is bars `, ); }); test("Issue #115 with layout, mixing pagination and collections", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/", output: "./test/stubs/_site", } }); let tmplFoos = await getNewTemplate( "./test/stubs/issue-115/template-foos.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmplBars = await getNewTemplate( "./test/stubs/issue-115/template-bars.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmplIndex = await getNewTemplate( "./test/stubs/issue-115/index-with-layout.liquid", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tm = new TemplateMap(eleventyConfig); await tm.add(tmplFoos); await tm.add(tmplBars); await tm.add(tmplIndex); await tm.cache(); let map = tm.getMap(); t.is(map.length, 3); t.deepEqual(map[2].template, tmplIndex); let collections = await tm._testGetAllCollectionsData(); t.is(Object.keys(collections.all).length, 3); t.is(Object.keys(collections.foos).length, 1); t.is(Object.keys(collections.bars).length, 1); t.is(Object.keys((await tm._testGetCollectionsData()).all).length, 3); t.is(Object.keys((await tm._testGetCollectionsData()).foos).length, 1); t.is(Object.keys((await tm._testGetCollectionsData()).bars).length, 1); t.truthy(map[0].data.collections); t.truthy(map[1].data.collections); t.truthy(map[2].data.collections); t.truthy(Object.keys(map[2].data.collections).length); t.is(Object.keys(map[0].data.collections.all).length, 3); t.is(Object.keys(map[0].data.collections.foos).length, 1); t.is(Object.keys(map[0].data.collections.bars).length, 1); t.is(Object.keys(map[1].data.collections.all).length, 3); t.is(Object.keys(map[1].data.collections.foos).length, 1); t.is(Object.keys(map[1].data.collections.bars).length, 1); t.is(Object.keys(map[2].data.collections.all).length, 3); t.is(Object.keys(map[2].data.collections.foos).length, 1); t.is(Object.keys(map[2].data.collections.bars).length, 1); let entry = await getRenderedTmpls(map[2].template, map[2].data); t.deepEqual( normalizeNewLines(entry[0].templateContent), `This page is foos This page is bars `, ); }); test("TemplateMap adds collections data and has page data values using .cache()", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/", output: "./test/stubs/_site", } }); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let map = tm.getMap(); await tm.cache(); t.is(map[0].data.page.url, "/templateMapCollection/test1/"); t.is(map[0].data.page.outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html"); t.is(map[0].data.page.inputPath, "./test/stubs/templateMapCollection/test1.md"); t.is(map[0].data.page.fileSlug, "test1"); t.truthy(map[0].data.page.date); }); test("TemplateMap adds collections data and has page data values using ._testGetCollectionsData()", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/", output: "./test/stubs/_site", } }); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); t.is(collections.all[0].url, "/templateMapCollection/test1/"); t.is(collections.all[0].outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html"); t.is(collections.all[0].data.page.url, "/templateMapCollection/test1/"); t.is( collections.all[0].data.page.outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html", ); t.is(collections.all[0].data.page.inputPath, "./test/stubs/templateMapCollection/test1.md"); t.is(collections.all[0].data.page.fileSlug, "test1"); }); test("Url should be available in user config collections API calls", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { return collection.getAll(); }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); t.truthy(collections.userCollection); t.truthy(collections.userCollection.length); t.is(collections.userCollection[0].url, "/templateMapCollection/test1/"); t.is( collections.userCollection[0].outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html", ); t.is(collections.userCollection[0].data.page.url, "/templateMapCollection/test1/"); t.is( collections.userCollection[0].data.page.outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html", ); }); test("Url should be available in user config collections API calls (test in callback)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getAll(); t.is(all[0].url, "/templateMapCollection/test1/"); t.is(all[0].outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html"); t.is(all[1].url, "/templateMapCollection/test2/"); t.is(all[1].outputPath, "./test/stubs/_site/templateMapCollection/test2/index.html"); return all; }); } ); let tm = new TemplateMap(eleventyConfig); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await tm.cache(); }); test("Should be able to paginate a tag generated collection", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-tag.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.truthy(collections.dog); t.truthy(collections.dog.length); }); test("Should be able to paginate a user config collection", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.truthy(collections.userCollection); t.truthy(collections.userCollection.length); }); test("Should be able to paginate a user config collection (uses rendered permalink)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); t.is(all[0].url, "/templateMapCollection/test1/"); t.is(all[0].outputPath, "./test/stubs/_site/templateMapCollection/test1/index.html"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg-permalink.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.truthy(collections.userCollection); t.truthy(collections.userCollection.length); let urls = []; for (let item of collections.all) { urls.push(item.url); } t.is(urls.indexOf("/test-title/hello/") > -1, true); }); test("Should be able to paginate a user config collection (paged template is also tagged)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg-tagged.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.is(collections.dog.length, 2); t.truthy(collections.haha); t.is(collections.haha.length, 1); t.is(collections.haha[0].url, "/templateMapCollection/paged-cfg-tagged/"); }); test("Should be able to paginate a user config collection (paged template is also tagged, add all pages to collections)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg-tagged-apply-to-all.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.is(collections.dog.length, 2); t.truthy(collections.haha); t.is(collections.haha.length, 2); t.is(collections.haha[0].url, "/templateMapCollection/paged-cfg-tagged-apply-to-all/"); t.is(collections.haha[1].url, "/templateMapCollection/paged-cfg-tagged-apply-to-all/1/"); }); test("Should be able to paginate a user config collection (paged template is also tagged, uses custom rendered permalink)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg-tagged-permalink.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.truthy(collections.haha); t.is(collections.haha.length, 1); t.is(collections.haha[0].url, "/test-title/goodbye/"); }); test("Should be able to paginate a user config collection (paged template is also tagged, uses custom rendered permalink, add all pages to collections)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-cfg-tagged-permalink-apply-to-all.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.truthy(collections.haha); t.is(collections.haha.length, 2); t.is(collections.haha[0].url, "/test-title/goodbye/"); t.is(collections.haha[1].url, "/test-title-4/goodbye/"); }); test("TemplateMap render and templateContent are the same (templateContent doesn’t have layout but makes proper use of layout front matter data)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/", output: "./test/stubs/_site", } }); let tm = new TemplateMap(eleventyConfig); let tmplLayout = await getNewTemplate( "./test/stubs/templateMapCollection/testWithLayout.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmplLayout); let map = tm.getMap(); await tm.cache(); t.is(map[0]._pages[0].templateContent.trim(), "

Inherited

"); t.is((await renderTemplate(map[0].template, map[0].data)).trim(), "

Inherited

"); }); test("Should be able to paginate a tag generated collection (and it has templateContent)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-tag-dogs-templateContent.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.cache(); let pagedMapEntry = tm.getMapEntryForInputPath( "./test/stubs/templateMapCollection/paged-tag-dogs-templateContent.md", ); let templates = await getRenderedTmpls(pagedMapEntry.template, pagedMapEntry.data); t.is(templates.length, 2); t.is(templates[0].data.pagination.pageNumber, 0); t.is(templates[1].data.pagination.pageNumber, 1); t.is( templates[0].templateContent.trim(), `

Before

Test 1

After

`, ); t.is( templates[1].templateContent.trim(), `

Before

Test 4

After

`, ); }); test("Should be able to paginate a tag generated collection when aliased (and it has templateContent)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); // has dog tag await tm.add(tmpl2); // does not have dog tag await tm.add(tmpl4); // has dog tag let pagedTmpl = await getNewTemplate( "./test/stubs/templateMapCollection/paged-tag-dogs-templateContent-alias.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.cache(); let pagedMapEntry = tm.getMapEntryForInputPath( "./test/stubs/templateMapCollection/paged-tag-dogs-templateContent-alias.md", ); let templates = await getRenderedTmpls(pagedMapEntry.template, pagedMapEntry.data); t.is(templates.length, 1); t.is(templates[0].data.pagination.pageNumber, 0); t.is( templates[0].templateContent.trim(), `

Before

Test 1

Test 4

After

`, ); }); test("Issue #253: Paginated template with a tag should put multiple pages into a collection", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", function (collection) { // TODO test user config collections (no actual tests against this collection yet) let all = collection.getFilteredByTag("dog"); return all; }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tmpl4 = await getNewTemplateByNumber(4, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await tm.add(tmpl4); let pagedTmpl = await getNewTemplate( "./test/stubs/tagged-pagination-multiples/test.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.is(collections.dog.length, 2); t.truthy(collections.haha); t.is(collections.haha.length, 2); t.is(collections.haha[0].url, "/tagged-pagination-multiples/test/"); t.is(collections.haha[1].url, "/tagged-pagination-multiples/test/1/"); }); test("getUserConfigCollectionNames", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addCollection("userCollection", function (collection) { return collection.getAll(); }); cfg.addCollection("otherUserCollection", function (collection) { return collection.getAll(); }); } ); let tm = new TemplateMap(eleventyConfig); t.deepEqual(tm.getUserConfigCollectionNames(), ["userCollection", "otherUserCollection"]); }); test("isUserConfigCollectionName", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addCollection("userCollection", function (collection) { return collection.getAll(); }); } ); let tm = new TemplateMap(eleventyConfig); t.is(tm.isUserConfigCollectionName("userCollection"), true); t.is(tm.isUserConfigCollectionName("userCollection2"), false); }); test("Dependency Map should have nodes that have no dependencies and no dependents", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl5); await tm.cache(); let deps = tm.getTemplateOrder(); t.true(deps.filter((dep) => dep.indexOf("test5.md") > -1).length > 0); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 2); }); test("Dependency Map should have include orphan user config collections (in the correct order) #3711", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addCollection("userCollection", function (collection) { return collection.getAll(); }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl5); await tm.cache(); let deps = tm.getTemplateOrder(); t.deepEqual(deps, [ './test/stubs/templateMapCollection/test1.md', '__collection:post', '__collection:dog', './test/stubs/templateMapCollection/test5.md', '__collection:userCollection', '__collection:[keys]', '__collection:all' ]); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 2); t.is(collections.userCollection.length, 2); }); test("Dependency Map should have include orphan user config collections, mapped by user collection api to tag #3711", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addCollection("userCollection", function (collection) { return collection.getFilteredByTag("post"); }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl5 = await getNewTemplateByNumber(5, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl5); await tm.cache(); let deps = tm.getTemplateOrder(); t.deepEqual(deps, [ './test/stubs/templateMapCollection/test1.md', '__collection:post', '__collection:dog', './test/stubs/templateMapCollection/test5.md', '__collection:userCollection', '__collection:[keys]', '__collection:all', ]); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 2); t.is(collections.userCollection.length, 1); }); test("Template pages should not have layouts when added to collections", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs/", output: "./test/stubs/_site", } }); let tm = new TemplateMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs/collection-layout-wrap.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmpl); t.is(await renderTemplate(tmpl, await tmpl.getData()), "
Layout Test
"); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 1); t.is(collections.all[0].templateContent, "Layout Test"); }); test("Paginated template pages should not have layouts when added to collections", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "test/stubs/_site", } }); let tm = new TemplateMap(eleventyConfig); let pagedTmpl = await getNewTemplate( "./test/stubs/tagged-pagination-multiples-layout/test.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 3); t.is(collections.all[0].templateContent, "one"); t.is(collections.all[1].templateContent, "two"); t.is(collections.all[2].templateContent, "three"); }); test("Tag pages. Allow pagination over all collections a la `data: collections`", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); let pagedTmpl = await getNewTemplate( "./test/stubs/page-target-collections/tagpages.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 3); let collectionTagPagesTemplateContents = new Set( collections.all .filter(function (entry) { return entry.inputPath.endsWith("tagpages.njk"); }) .map(function (entry) { return entry.templateContent.trim(); }), ); t.deepEqual(collectionTagPagesTemplateContents, new Set(["post"])); }); test("Tag pages (all pages added to collections). Allow pagination over all collections a la `data: collections`", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); let pagedTmpl = await getNewTemplate( "./test/stubs/page-target-collections/tagpagesall.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 5); let collectionTagPagesTemplateContents = new Set( collections.all .filter(function (entry) { return entry.inputPath.endsWith("tagpagesall.njk"); }) .map(function (entry) { return entry.templateContent.trim(); }), ); t.deepEqual(collectionTagPagesTemplateContents, new Set(["post", "dog", "cat"])); }); test("eleventyExcludeFromCollections", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); let excludedTmpl = await getNewTemplate( "./test/stubs/eleventyExcludeFromCollections.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(excludedTmpl); await tm.cache(); t.is(tm.getMap().length, 2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 1); t.is(collections.post.length, 1); t.is(collections.dog.length, 1); }); test("eleventyExcludeFromCollections and permalink: false", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); let excludedTmpl = await getNewTemplate( "./test/stubs/eleventyExcludeFromCollectionsPermalinkFalse.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(excludedTmpl); await tm.cache(); t.is(tm.getMap().length, 2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 1); t.is(collections.post.length, 1); t.is(collections.dog.length, 1); }); test("Paginate over collections.all", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); let pagedTmpl = await getNewTemplate( "./test/stubs/page-target-collections/paginateall.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 4); t.is( collections.all.filter(function (entry) { return entry.inputPath.endsWith("test1.md"); }).length, 1, ); t.is( collections.all.filter(function (entry) { return entry.inputPath.endsWith("test2.md"); }).length, 1, ); t.is( collections.all.filter(function (entry) { return entry.inputPath.endsWith("paginateall.njk"); }).length, 2, ); let map = tm.getMap(); t.is(map[0].inputPath, "./test/stubs/page-target-collections/paginateall.njk"); t.is(map[0]._pages.length, 2); t.is(map[0]._pages[0].templateContent, "INPUT PATH:./test/stubs/templateMapCollection/test1.md"); t.is(map[0]._pages[1].templateContent, "INPUT PATH:./test/stubs/templateMapCollection/test2.md"); t.is(map[1].inputPath, "./test/stubs/templateMapCollection/test1.md"); t.is(map[1]._pages[0].templateContent.trim(), "

Test 1

"); t.is(map[2].inputPath, "./test/stubs/templateMapCollection/test2.md"); t.is(map[2]._pages[0].templateContent.trim(), "

Test 2

"); }); test("Paginate over collections (tag pages) related to #3823", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); let tagPagesTmpl = await getNewTemplate( "./test/stubs/page-target-collections/tagpagesall.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tagPagesTmpl); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); // 2 individual templates, 3 pages for tagpagesall t.deepEqual(collections.all.map(entry => entry.url).sort(), [ '/test/stubs/page-target-collections/tagpagesall/', '/test/stubs/page-target-collections/tagpagesall/1/', '/test/stubs/page-target-collections/tagpagesall/2/', '/test/stubs/templateMapCollection/test1/', '/test/stubs/templateMapCollection/test2/', ]); }); test("Paginate over collections.all WITH a paginate over collections (tag pages) related to #3823", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tmpl2 = await getNewTemplateByNumber(2, eleventyConfig); let tm = new TemplateMap(eleventyConfig); let pagedTmpl = await getNewTemplate( "./test/stubs/page-target-collections/paginateall.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tagPagesTmpl = await getNewTemplate( "./test/stubs/page-target-collections/tagpagesall.njk", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(pagedTmpl); await tm.add(tagPagesTmpl); await tm.add(tmpl1); await tm.add(tmpl2); let collections = await tm._testGetCollectionsData(); // 2 individual templates, 3 pages for tagpagesall, 5 pages from paginateall to paginate the 2+3 t.is(collections.all.length, 10); }); test("Test a transform with a layout (via templateMap)", async (t) => { t.plan(7); let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "./test/stubs-475/", output: "./test/stubs-475/_site", } }); let tm = new TemplateMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-475/transform-layout/transform-layout.njk", "./test/stubs-475/", "./test/stubs-475/_site", eleventyConfig, ); tmpl.addLinter(function (content, inputPath, outputPath) { // should be pre-transform content t.is(content, "This is content."); t.true(inputPath.endsWith("transform-layout.njk")); t.true(outputPath.endsWith("transform-layout/index.html")); }); tmpl.setTransforms({ transformName: function (content, outputPath) { t.is(content, "This is content."); t.true(outputPath.endsWith("transform-layout/index.html")); return "OVERRIDE BY A TRANSFORM"; } }); await tm.add(tmpl); await tm.cache(); t.is(tm.getMap().length, 1); for (let entry of tm.getMap()) { for (let page of entry._pages) { t.is(await page.template.renderPageEntry(page), "OVERRIDE BY A TRANSFORM"); } } }); test("Async user collection addCollection method", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs/", output: "./test/stubs/_site", }, function(cfg) { cfg.addCollection("userCollection", async function (collection) { return new Promise((resolve) => { setTimeout(function () { resolve(collection.getAll()); }, 50); }); }); } ); let tmpl1 = await getNewTemplateByNumber(1, eleventyConfig); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); let collections = await tm._testGetCollectionsData(); t.is(collections.userCollection[0].url, "/templateMapCollection/test1/"); t.is(collections.userCollection[0].data.collections.userCollection.length, 1); }); test("Duplicate permalinks in template map", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplate( "./test/stubs/permalink-conflicts/test1.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmpl2 = await getNewTemplate( "./test/stubs/permalink-conflicts/test2.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await t.throwsAsync(async () => { await tm.cache(); }); }); test("No duplicate permalinks in template map, using false", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplate( "./test/stubs/permalink-conflicts-false/test1.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmpl2 = await getNewTemplate( "./test/stubs/permalink-conflicts-false/test2.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl2); await tm.cache(); t.true(true); }); test("Duplicate permalinks in template map, no leading slash", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tmpl1 = await getNewTemplate( "./test/stubs/permalink-conflicts/test1.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tmpl3 = await getNewTemplate( "./test/stubs/permalink-conflicts/test3.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); let tm = new TemplateMap(eleventyConfig); await tm.add(tmpl1); await tm.add(tmpl3); await t.throwsAsync(async () => { await tm.cache(); }); }); test("TemplateMap circular references (map.templateContent) using eleventyExcludeFromCollections and collections.all", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tm = new TemplateMap(eleventyConfig); let tmplExcluded = await getNewTemplate( "./test/stubs/issue-522/excluded.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmplExcluded); let tmpl = await getNewTemplate( "./test/stubs/issue-522/template.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmpl); let map = tm.getMap(); t.falsy(map[0].data.collections); await tm.cache(); let deps = tm.getTemplateOrder(); t.deepEqual(deps, [ "./test/stubs/issue-522/excluded.md", "./test/stubs/issue-522/template.md", "__collection:[keys]", "__collection:all", ]); t.is(tm.getMap().length, 2); let collections = await tm._testGetCollectionsData(); t.is(collections.all.length, 1); }); test("permalink object with build", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tm = new TemplateMap(eleventyConfig); let tmplLayout = await getNewTemplate( "./test/stubs/permalink-build/permalink-build.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmplLayout); let map = tm.getMap(); await tm.cache(); t.is(map[0]._pages.length, 1); }); test("permalink object without build (defaults to `read` mode)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tm = new TemplateMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs/permalink-nobuild/permalink-nobuild.md", "./test/stubs/", "./test/stubs/_site", eleventyConfig, ); await tm.add(tmpl); let map = tm.getMap(); await tm.cache(); t.is(map[0]._pages.length, 1); t.throws( () => { map[0]._pages[0].templateContent; }, { instanceOf: TemplateContentUnrenderedTemplateError, }, ); }); test("eleventy.layouts Event", async (t) => { t.plan(1); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "./test/stubs-layouts-event/", output: "./test/stubs-layouts-event/_site", }, function(cfg) { cfg.on("eleventy.layouts", (layoutMap) => { t.deepEqual(layoutMap, { "./test/stubs-layouts-event/_includes/first.liquid": ["./test/stubs-layouts-event/page.md"], "./test/stubs-layouts-event/_includes/second.liquid": [ "./test/stubs-layouts-event/page.md", "./test/stubs-layouts-event/_includes/first.liquid", ], "./test/stubs-layouts-event/_includes/third.liquid": [ "./test/stubs-layouts-event/page.md", "./test/stubs-layouts-event/_includes/first.liquid", "./test/stubs-layouts-event/_includes/second.liquid", ], }); }); } ); let tm = new TemplateMap(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-layouts-event/page.md", "./test/stubs-layouts-event/", "./test/stubs-layouts-event/_site", eleventyConfig, ); await tm.add(tmpl); await tm.cache(); }); ================================================ FILE: test/TemplatePassthroughManagerTest.js ================================================ import test from "ava"; import fs from "fs"; import TemplatePassthroughManager from "../src/TemplatePassthroughManager.js"; import TemplateConfig from "../src/TemplateConfig.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import ProjectDirectories from "../src/Util/ProjectDirectories.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback, getEleventyFilesInstance, deleteDirectory } from "./_testHelpers.js"; test("Get paths from Config", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.passthroughCopies = { img: { outputPath: true }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); t.deepEqual(mgr.getConfigPaths(), [{ inputPath: "./img", outputPath: true, copyOptions: {} }]); }); test("isPassthroughCopyFile", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.passthroughCopies = { img: { outputPath: true }, fonts: { outputPath: true }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); t.false(mgr.isPassthroughCopyFile([])); t.false(mgr.isPassthroughCopyFile([], "")); t.false(mgr.isPassthroughCopyFile([], null)); t.truthy(mgr.isPassthroughCopyFile([], "./img/test.png")); t.deepEqual(mgr.isPassthroughCopyFile([], "./img/test.png"), { inputPath: "./img", outputPath: true, copyOptions: {}, }); t.truthy(mgr.isPassthroughCopyFile([], "./fonts/Roboto.woff")); t.deepEqual(mgr.isPassthroughCopyFile([], "./fonts/Roboto.woff"), { inputPath: "./fonts", outputPath: true, copyOptions: {}, }); t.false(mgr.isPassthroughCopyFile([], "./docs/test.njk")); t.false(mgr.isPassthroughCopyFile([], "./other-dir/test.png")); t.true(mgr.isPassthroughCopyFile(["hi", "./other-dir/test.png"], "./other-dir/test.png")); }); test("Get glob paths from config", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.passthroughCopies = { "test/stubs/img": { outputPath: true }, "test/stubs/img/**": { outputPath: "./" }, "test/stubs/img/*.js": { outputPath: "./" }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); t.deepEqual(mgr.getConfigPathGlobs(), [ "./test/stubs/img/**", "./test/stubs/img/**", "./test/stubs/img/*.js", ]); }); test("Get file paths", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); t.deepEqual(mgr.getNonTemplatePaths(["test.png"]), ["test.png"]); }); test("Get file paths (filter out real templates)", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); mgr.extensionMap.setFormats(["njk"]); t.deepEqual(mgr.getNonTemplatePaths(["test.njk"]), []); }); test("Get file paths (filter out real templates), multiple", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); mgr.extensionMap.setFormats(["njk"]); t.deepEqual(mgr.getNonTemplatePaths(["test.njk", "test.png"]), ["test.png"]); }); test("Get file paths with a js file (filter out real templates), multiple", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); mgr.extensionMap.setFormats(["njk"]); t.deepEqual(mgr.getNonTemplatePaths(["test.njk", "test.js"]), ["test.js"]); }); // This test used to be for passthroughFileCopy: false in config test("Get file paths (one image path)", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.extensionMap = new EleventyExtensionMap(eleventyConfig); t.deepEqual(mgr.getNonTemplatePaths(["test.png"]), ["test.png"]); }); test("Naughty paths outside of project dir", async (t) => { let dirs = new ProjectDirectories(); dirs.setInput("./test/stubs/template-passthrough2/"); dirs.setOutput("./test/stubs/template-passthrough2/_site/"); let eleventyConfig = new TemplateConfig(); eleventyConfig.setDirectories(dirs); eleventyConfig.userConfig.passthroughCopies = { "../static": { outputPath: true }, "../*": { outputPath: "./" }, "./test/stubs/template-passthrough2/static/*.css": { outputPath: "./" }, "./test/stubs/template-passthrough2/static/*.js": { outputPath: "../../" }, "./test/stubs/template-passthrough2/img.jpg": { outputPath: "../../" }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.setFileSystemSearch(new FileSystemSearch()); await t.throwsAsync(async function () { for (let path of mgr.getConfigPaths()) { let pass = mgr.getTemplatePassthroughForPath(path); await mgr.copyPassthrough(pass); } }, { message: `Having trouble copying './test/stubs/template-passthrough2/static/*.js'` }); const output = [ "./test/stubs/template-passthrough2/_site/static", "./test/stubs/template-passthrough2/_site/nope.txt", "./test/stubs/template-passthrough2/_site/nope/", "./test/stubs/test.js", "./test/stubs/img.jpg", ]; for (let path of output) { t.false(fs.existsSync(path)); } }); test("getAllNormalizedPaths", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.passthroughCopies = { img: { outputPath: true }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); t.deepEqual(mgr.getAllNormalizedPaths(), [ { inputPath: "./img", outputPath: true, copyOptions: {} }, ]); }); test("getAllNormalizedPaths with globs", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.passthroughCopies = { img: { outputPath: true }, "img/**": { outputPath: "./" }, "img/*.js": { outputPath: "./" }, }; await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); t.deepEqual(mgr.getAllNormalizedPaths(), [ { inputPath: "./img", outputPath: true, copyOptions: {} }, { inputPath: "./img/**", outputPath: "", copyOptions: {} }, { inputPath: "./img/*.js", outputPath: "", copyOptions: {} }, ]); }); test("getAliasesFromPassthroughResults with Unicode filenames", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let mgr = new TemplatePassthroughManager(eleventyConfig); let results = [ { map: { "./path/file": "_site/path/file" } }, { map: { "./测试.用例/⌘": "_site/测试.用例/⌘" } }, { map: { "./path/测试.用例": "_site/path/测试.用例" } }, { map: { "./测试.用例/file": "_site/测试.用例/file" } }, ] t.deepEqual(mgr.getAliasesFromPassthroughResults(results), { "/path/file": "./path/file", "/%E6%B5%8B%E8%AF%95.%E7%94%A8%E4%BE%8B/%E2%8C%98": "./测试.用例/⌘", "/path/%E6%B5%8B%E8%AF%95.%E7%94%A8%E4%BE%8B": "./path/测试.用例", "/%E6%B5%8B%E8%AF%95.%E7%94%A8%E4%BE%8B/file": "./测试.用例/file", }); }); test("Look for uniqueness on template passthrough paths #1677", async (t) => { let formats = []; let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/template-passthrough-duplicates/", output: "test/stubs/template-passthrough-duplicates/_site" }, function(cfg) { cfg.passthroughCopies = { "./test/stubs/template-passthrough-duplicates/input/**/*.png": { outputPath: "./", }, }; }); let { passthroughManager } = getEleventyFilesInstance(formats, eleventyConfig); await t.throwsAsync(async function () { await passthroughManager.copyAll(); }, { message: `Multiple passthrough copy files are trying to write to the same output file (./test/stubs/template-passthrough-duplicates/_site/avatar.png). ./test/stubs/template-passthrough-duplicates/input/avatar.png and ./test/stubs/template-passthrough-duplicates/input/src/views/avatar.png` }); deleteDirectory("test/stubs/template-passthrough-duplicates/_site/"); }); test("Incremental passthrough, issue #3285", async (t) => { let eleventyConfig = new TemplateConfig(); eleventyConfig.userConfig.addPassthroughCopy({ './test/stubs-3285/src/scripts': 'scripts' }); await eleventyConfig.init(); let mgr = new TemplatePassthroughManager(eleventyConfig); mgr.setIncrementalFile("./test/stubs-3285/src/scripts/hello-world.js"); t.deepEqual(mgr.getAllNormalizedPaths([]), [ { copyOptions: {}, inputPath: "./test/stubs-3285/src/scripts", outputPath: "scripts" }, ]); }); ================================================ FILE: test/TemplatePassthroughTest.js ================================================ import test from "ava"; import FileSystemSearch from "../src/FileSystemSearch.js"; import TemplatePassthrough from "../src/TemplatePassthrough.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getTemplatePassthrough(path, outputDir, inputDir) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir, output: outputDir, } }); if (typeof path === "object") { let p = new TemplatePassthrough(path, eleventyConfig); p.setFileSystemSearch(new FileSystemSearch()); return p; } let p = new TemplatePassthrough( { inputPath: path, outputPath: true }, eleventyConfig ); p.setFileSystemSearch(new FileSystemSearch()); return p; } test("Constructor", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "."); t.truthy(pass); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/avatar.png"); }); test("Constructor, input directory in inputPath is stripped", async (t) => { let pass = await getTemplatePassthrough("src/avatar.png", "_site", "src"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/avatar.png"); let pass2 = await getTemplatePassthrough( { inputPath: "src/avatar.png", outputPath: "avatar.png" }, "_site", "src" ); t.is(pass2.outputPath, "avatar.png"); t.is(await pass2.getOutputPath(), "_site/avatar.png"); }); test("Constructor, input directory in inputPath is stripped, duplicate directory names", async (t) => { let pass = await getTemplatePassthrough("src/src/avatar.png", "_site", "src"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/src/avatar.png"); let pass2 = await getTemplatePassthrough( { inputPath: "src/src/avatar.png", outputPath: "src/avatar.png" }, "_site", "src" ); t.is(pass2.outputPath, "src/avatar.png"); t.is(await pass2.getOutputPath(), "_site/src/avatar.png"); }); test("Constructor, input directory (object param, directory)", async (t) => { let pass = await getTemplatePassthrough( { inputPath: "src/test", outputPath: "test" }, "_site", "src" ); t.is(pass.outputPath, "test"); t.is(await pass.getOutputPath(), "_site/test"); }); test("Constructor, input directory, path missing input directory", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "src"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/avatar.png"); let pass2 = await getTemplatePassthrough( { inputPath: "avatar.png", outputPath: "avatar.png" }, "_site", "src" ); t.is(pass2.outputPath, "avatar.png"); t.is(await pass2.getOutputPath(), "_site/avatar.png"); }); test("Constructor not Dry Run", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "."); t.is(pass.outputPath, true); t.is(pass.isDryRun, false); }); test("Constructor Dry Run", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "."); pass.setDryRun(true); t.is(pass.outputPath, true); t.is(pass.isDryRun, true); }); test("Origin path isn’t included in output when targeting a directory", async (t) => { let pass = await getTemplatePassthrough("img", "_site", "test/stubs"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/img"); }); test("Origin path isn’t included in output when targeting a directory several levels deep", async (t) => { let pass = await getTemplatePassthrough("img", "_site", "test/stubs/subdir"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/img"); }); test("Target directory’s subdirectory structure is retained", async (t) => { let pass = await getTemplatePassthrough("subdir/img", "_site", "test/stubs"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/subdir/img"); let pass2 = await getTemplatePassthrough( { inputPath: "subdir/img", outputPath: "subdir/img" }, "_site", "test/stubs" ); t.is(await pass2.getOutputPath(), "_site/subdir/img"); }); test("Origin path isn’t included in output when targeting a file", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "test/stubs"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/avatar.png"); }); test("Origin path isn’t included in output when targeting a file several levels deep", async (t) => { let pass = await getTemplatePassthrough("avatar.png", "_site", "test/stubs/subdir/img"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/avatar.png"); }); test("Full input file path and deep input path", async (t) => { let tp = await getTemplatePassthrough("test/views/avatar.png", "_site", "test/views/"); t.is(await tp.getOutputPath(), "_site/avatar.png"); let tp2 = await getTemplatePassthrough("test/views/avatar.png", "_site", "test/views"); t.is(await tp2.getOutputPath(), "_site/avatar.png"); let tp3 = await getTemplatePassthrough("test/views/avatar.png", "_site/", "test/views"); t.is(await tp3.getOutputPath(), "_site/avatar.png"); let tp4 = await getTemplatePassthrough("test/views/avatar.png", "./_site", "./test/views"); t.is(await tp4.getOutputPath(), "_site/avatar.png"); let tp5 = await getTemplatePassthrough("./test/views/avatar.png", "./_site/", "./test/views/"); t.is(await tp5.getOutputPath(), "_site/avatar.png"); let tp6 = await getTemplatePassthrough("./test/views/avatar.png", "_site", "test/views/"); t.is(await tp6.getOutputPath(), "_site/avatar.png"); }); test(".htaccess", async (t) => { let pass = await getTemplatePassthrough(".htaccess", "_site", "."); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/.htaccess"); }); test(".htaccess with input dir", async (t) => { let pass = await getTemplatePassthrough(".htaccess", "_site", "test/stubs"); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/.htaccess"); }); test("getFiles where not glob and file does not exist", async (t) => { const inputPath = ".htaccess"; let pass = await getTemplatePassthrough(inputPath, "_site", "test/stubs"); t.is(pass.outputPath, true); const files = await pass.getFiles(inputPath); t.deepEqual(files, []); }); test("getFiles where not glob and directory does not exist", async (t) => { const inputPath = "./test/stubs/template-passthrough/static/not-exists/"; let pass = await getTemplatePassthrough(inputPath, "_site", "test/stubs"); t.is(pass.outputPath, true); const files = await pass.getFiles(inputPath); t.deepEqual(files, []); }); test("getFiles with glob", async (t) => { const inputPath = "./test/stubs/template-passthrough/static/**"; let pass = await getTemplatePassthrough(inputPath, "_site", "test/views"); t.is(pass.outputPath, true); const files = await pass.getFiles(inputPath); t.deepEqual( files.sort(), [ "./test/stubs/template-passthrough/static/test.css", "./test/stubs/template-passthrough/static/test.js", "./test/stubs/template-passthrough/static/nested/test-nested.css", ].sort() ); t.is( await pass.getOutputPath(files.filter((entry) => entry.endsWith("test.css"))[0]), "_site/test/stubs/template-passthrough/static/test.css" ); t.is( await pass.getOutputPath(files.filter((entry) => entry.endsWith("test.js"))[0]), "_site/test/stubs/template-passthrough/static/test.js" ); t.is( await pass.getOutputPath(files.filter((entry) => entry.endsWith("test-nested.css"))[0]), "_site/test/stubs/template-passthrough/static/nested/test-nested.css" ); }); test("getFiles with glob 2", async (t) => { const inputPath = "./test/stubs/template-passthrough/static/**/*.js"; let pass = await getTemplatePassthrough(inputPath, "_site", "test/views"); t.is(pass.outputPath, true); const files = await pass.getFiles(inputPath); t.deepEqual(files, ["./test/stubs/template-passthrough/static/test.js"]); t.is(await pass.getOutputPath(files[0]), "_site/test/stubs/template-passthrough/static/test.js"); }); test("Directory where outputPath is true", async (t) => { let pass = await getTemplatePassthrough( { inputPath: "./static", outputPath: true }, "_site", "test/stubs" ); t.is(pass.outputPath, true); t.is(await pass.getOutputPath(), "_site/static"); }); test("Nested directory where outputPath is remapped", async (t) => { let pass = await getTemplatePassthrough( { inputPath: "./static/nested", outputPath: "./test" }, "_site", "test/stubs" ); t.is(pass.outputPath, "./test"); t.is(await pass.getOutputPath(), "_site/test"); }); test("Glob pattern", async (t) => { const globResolvedPath = "./test/stubs/template-passthrough/static/test.js"; let pass = await getTemplatePassthrough( { inputPath: "./test/stubs/template-passthrough/static/*.js", outputPath: "./directory/", }, "_site", "test/stubs" ); t.is(pass.outputPath, "./directory/"); t.is(await pass.getOutputPath(globResolvedPath), "_site/directory/test.js"); }); test("Output paths match with different templatePassthrough methods", async (t) => { let pass1 = await getTemplatePassthrough( { inputPath: "./static/nested", outputPath: "./test" }, "_site", "test/stubs" ); let pass2 = await getTemplatePassthrough("avatar.png", "_site/test", "."); t.is(await pass1.getOutputPathForGlobFile("avatar.png"), await pass2.getOutputPath()); }); // ToDo: Currently can't do :( // test("File renamed", async t => { // let pass = await getTemplatePassthrough( // { // inputPath: "./test/stubs/template-passthrough/static/test.js", // outputPath: "./rename.js" // }, // "_site", // "test/stubs" // ); // t.truthy(pass); // t.is(await pass.getOutputPath(), "_site/rename.js"); // }); test("Bug with incremental file copying to a directory output, issue #2278 #1038", async (t) => { let pass1 = await getTemplatePassthrough( { inputPath: "./test/stubs/public/test.css", outputPath: "/" }, "test/stubs", "." ); t.is(await pass1.getOutputPath(), "test/stubs/test.css"); }); test("Bug with incremental dir copying to a directory output, issue #2278 #1038", async (t) => { let pass1 = await getTemplatePassthrough( { inputPath: "./test/stubs/public/", outputPath: "/" }, "test/stubs", "." ); t.is(await pass1.getOutputPath(), "./test/stubs/"); }); ================================================ FILE: test/TemplatePermalinkTest.js ================================================ import test from "ava"; import TemplatePermalink from "../src/TemplatePermalink.js"; const { generate } = TemplatePermalink; test("Simple straight permalink", (t) => { t.is( new TemplatePermalink("permalinksubfolder/test.html").toOutputPath(), "permalinksubfolder/test.html" ); t.is( new TemplatePermalink("./permalinksubfolder/test.html").toOutputPath(), "permalinksubfolder/test.html" ); t.is( new TemplatePermalink("permalinksubfolder/test.html").toHref(), "/permalinksubfolder/test.html" ); t.is( new TemplatePermalink("./permalinksubfolder/test.html").toHref(), "/permalinksubfolder/test.html" ); t.is(new TemplatePermalink("./testindex.html").toHref(), "/testindex.html"); t.is( new TemplatePermalink("./permalinksubfolder/testindex.html").toHref(), "/permalinksubfolder/testindex.html" ); }); test("Permalink without filename", (t) => { t.is( new TemplatePermalink("permalinksubfolder/").toOutputPath(), "permalinksubfolder/index.html" ); t.is( new TemplatePermalink("./permalinksubfolder/").toOutputPath(), "permalinksubfolder/index.html" ); t.is( new TemplatePermalink("/permalinksubfolder/").toOutputPath(), "/permalinksubfolder/index.html" ); t.is(new TemplatePermalink("permalinksubfolder/").toHref(), "/permalinksubfolder/"); t.is(new TemplatePermalink("./permalinksubfolder/").toHref(), "/permalinksubfolder/"); t.is(new TemplatePermalink("/permalinksubfolder/").toHref(), "/permalinksubfolder/"); }); test("Permalink with pagination subdir", (t) => { t.is( new TemplatePermalink("permalinksubfolder/test.html", "0/").toOutputPath(), "permalinksubfolder/0/test.html" ); t.is( new TemplatePermalink("permalinksubfolder/test.html", "1/").toOutputPath(), "permalinksubfolder/1/test.html" ); t.is( new TemplatePermalink("permalinksubfolder/test.html", "0/").toHref(), "/permalinksubfolder/0/test.html" ); t.is( new TemplatePermalink("permalinksubfolder/test.html", "1/").toHref(), "/permalinksubfolder/1/test.html" ); }); test("Permalink generate", (t) => { t.is(generate("./", "index").toOutputPath(), "index.html"); t.is(generate("./", "index").toHref(), "/"); t.is(generate(".", "index").toOutputPath(), "index.html"); t.is(generate(".", "index").toHref(), "/"); t.is(generate(".", "test").toOutputPath(), "test/index.html"); t.is(generate(".", "test").toHref(), "/test/"); t.is(generate(".", "test", "0/").toOutputPath(), "test/0/index.html"); t.is(generate(".", "test", "0/").toHref(), "/test/0/"); t.is(generate(".", "test", "1/").toOutputPath(), "test/1/index.html"); t.is(generate(".", "test", "1/").toHref(), "/test/1/"); }); test("Permalink generate with suffix", (t) => { t.is(generate(".", "test", null).toOutputPath(), "test/index.html"); t.is(generate(".", "test", null).toHref(), "/test/"); t.is(generate(".", "test", "1/").toOutputPath(), "test/1/index.html"); t.is(generate(".", "test", "1/").toHref(), "/test/1/"); }); test("Permalink generate with new extension", (t) => { t.is(generate(".", "test", null, "css").toOutputPath(), "test.css"); t.is(generate(".", "test", null, "css").toHref(), "/test.css"); t.is(generate(".", "test", "1/", "css").toOutputPath(), "1/test.css"); t.is(generate(".", "test", "1/", "css").toHref(), "/1/test.css"); }); test("Permalink generate with subfolders", (t) => { t.is(generate("permalinksubfolder/", "index").toOutputPath(), "permalinksubfolder/index.html"); t.is( generate("permalinksubfolder/", "test").toOutputPath(), "permalinksubfolder/test/index.html" ); t.is( generate("permalinksubfolder/", "test", "1/").toOutputPath(), "permalinksubfolder/test/1/index.html" ); t.is(generate("permalinksubfolder/", "index").toHref(), "/permalinksubfolder/"); t.is(generate("permalinksubfolder/", "test").toHref(), "/permalinksubfolder/test/"); t.is( generate("permalinksubfolder/", "test", "1/").toHref(), "/permalinksubfolder/test/1/" ); }); test("Permalink matching folder and filename", (t) => { let hasDupe = TemplatePermalink._hasDuplicateFolder; t.is(hasDupe("subfolder", "component"), false); t.is(hasDupe("subfolder/", "component"), false); t.is(hasDupe(".", "component"), false); t.is(hasDupe("component", "component"), true); t.is(hasDupe("component/", "component"), true); t.is(generate("component/", "component").toOutputPath(), "component/index.html"); t.is(generate("component/", "component").toHref(), "/component/"); }); test("Permalink Object, just build", (t) => { t.is( new TemplatePermalink({ build: "permalinksubfolder/test.html", }).toOutputPath(), "permalinksubfolder/test.html" ); t.is( new TemplatePermalink({ build: false, }).toOutputPath(), false ); t.throws(() => { new TemplatePermalink({ build: true, }).toOutputPath(); }); }); test("Permalink Object, empty object", (t) => { t.is(new TemplatePermalink({}).toOutputPath(), false); t.is(new TemplatePermalink({}).toHref(), false); }); test("Permalink generate apache content negotiation #761", (t) => { let tp = new TemplatePermalink("index.es.html"); tp.setUrlTransforms([ function ({ url }) { return "/"; }, ]); t.is(tp.toHref(), "/"); t.is(tp.toOutputPath(), "index.es.html"); // Note that generate does some preprocessing to the raw permalink value (compared to `new TemplatePermalink`) let tp1 = TemplatePermalink.generate("", "index.es"); tp1.setUrlTransforms([ function ({ url }) { return "/"; }, ]); t.is(tp1.toHref(), "/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for index.es.html t.is(tp1.toOutputPath(), "index.es/index.html"); }); test("Permalink generate apache content negotiation with subdirectory #761", (t) => { let tp = new TemplatePermalink("test/index.es.html"); tp.setUrlTransforms([ function ({ url }) { return "/test/"; }, ]); t.is(tp.toHref(), "/test/"); t.is(tp.toOutputPath(), "test/index.es.html"); // Note that generate does some preprocessing to the raw permalink value (compared to `new TemplatePermalink`) let tp1 = TemplatePermalink.generate("test", "index.es"); tp1.setUrlTransforms([ function ({ url }) { return "/test/"; }, ]); t.is(tp1.toHref(), "/test/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for test/index.es.html t.is(tp1.toOutputPath(), "test/index.es/index.html"); }); test("Permalink generate apache content negotiation non-index file name #761", (t) => { // Note that generate does some preprocessing to the raw permalink value (compared to `new TemplatePermalink`) let tp = TemplatePermalink.generate("permalinksubfolder", "about.es"); t.is(tp.toHref(), "/permalinksubfolder/about.es/"); t.is(tp.toOutputPath(), "permalinksubfolder/about.es/index.html"); }); test("Permalink generate with urlTransforms #761", (t) => { // Note that TemplatePermalink.generate is used by Template and different from new TemplatePermalink let tp = TemplatePermalink.generate("permalinksubfolder", "index.es"); tp.setUrlTransforms([ function ({ url }) { return "/permalinksubfolder/"; }, ]); t.is(tp.toHref(), "/permalinksubfolder/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for permalinksubfolder/index.es.html t.is(tp.toOutputPath(), "permalinksubfolder/index.es/index.html"); }); test("Permalink generate with urlTransforms (skip via undefined) #761", (t) => { // Note that TemplatePermalink.generate is used by Template and different from new TemplatePermalink let tp = TemplatePermalink.generate("permalinksubfolder", "index.es"); tp.setUrlTransforms([ function ({ url }) { // return nothing }, ]); t.is(tp.toHref(), "/permalinksubfolder/index.es/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for permalinksubfolder/index.es.html t.is(tp.toOutputPath(), "permalinksubfolder/index.es/index.html"); }); test("Permalink generate with 2 urlTransforms #761", (t) => { // Note that TemplatePermalink.generate is used by Template and different from new TemplatePermalink let tp = TemplatePermalink.generate("permalinksubfolder", "index.es"); tp.setUrlTransforms([ function ({ url }) { return "/abc/"; }, function ({ url }) { return "/def/"; }, ]); t.is(tp.toHref(), "/def/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for permalinksubfolder/index.es.html t.is(tp.toOutputPath(), "permalinksubfolder/index.es/index.html"); }); test("Permalink generate with urlTransforms returns index.html #761", (t) => { // Note that TemplatePermalink.generate is used by Template and different from new TemplatePermalink let tp = TemplatePermalink.generate("permalinksubfolder", "index.es"); tp.setUrlTransforms([ function ({ url }) { return "/abc/index.html"; }, ]); t.is(tp.toHref(), "/abc/"); // best paired with https://v3.11ty.dev/docs/data-eleventy-supplied/#filepathstem for permalinksubfolder/index.es.html t.is(tp.toOutputPath(), "permalinksubfolder/index.es/index.html"); }); test("Permalink generate with urlTransforms code (index file) #761", (t) => { let tp1 = new TemplatePermalink("index.es.html"); tp1.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/index.es.html"); t.is(urlStem, "/index.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // trailing slash return url.slice(0, -1 * ".en.html".length) + "/"; } }, ]); t.is(tp1.toHref(), "/"); let tp2 = new TemplatePermalink("index.es.html"); tp2.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/index.es.html"); t.is(urlStem, "/index.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // no trailing slash return url.slice(0, -1 * ".en.html".length); } }, ]); t.is(tp2.toHref(), "/"); }); test("Permalink generate with urlTransforms code (not index file) #761", (t) => { let tp1 = new TemplatePermalink("about.es.html"); tp1.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/about.es.html"); t.is(urlStem, "/about.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // trailing slash return url.slice(0, -1 * ".en.html".length) + "/"; } }, ]); t.is(tp1.toHref(), "/about/"); let tp2 = new TemplatePermalink("about.es.html"); tp2.setUrlTransforms([ function ({ url }) { if (url.match(/\.[a-z]{2}\.html$/i)) { // no trailing slash return url.slice(0, -1 * ".en.html".length); } }, ]); t.is(tp2.toHref(), "/about"); }); test("Permalink generate with urlTransforms code (index file with subdir) #761", (t) => { let tp1 = new TemplatePermalink("subdir/index.es.html"); tp1.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/subdir/index.es.html"); t.is(urlStem, "/subdir/index.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // trailing slash return url.slice(0, -1 * ".en.html".length) + "/"; } }, ]); t.is(tp1.toHref(), "/subdir/"); let tp2 = new TemplatePermalink("subdir/index.es.html"); tp2.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/subdir/index.es.html"); t.is(urlStem, "/subdir/index.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // no trailing slash return url.slice(0, -1 * ".en.html".length); } }, ]); t.is(tp2.toHref(), "/subdir/"); }); test("Permalink generate with urlTransforms code (not-index file with subdir) #761", (t) => { let tp1 = new TemplatePermalink("subdir/about.es.html"); tp1.setUrlTransforms([ function ({ url, urlStem }) { if (url.match(/\.[a-z]{2}\.html$/i)) { t.is(url, "/subdir/about.es.html"); t.is(urlStem, "/subdir/about.es"); // trailing slash return url.slice(0, -1 * ".en.html".length) + "/"; } }, ]); t.is(tp1.toHref(), "/subdir/about/"); let tp2 = new TemplatePermalink("subdir/about.es.html"); tp2.setUrlTransforms([ function ({ url, urlStem }) { t.is(url, "/subdir/about.es.html"); t.is(urlStem, "/subdir/about.es"); if (url.match(/\.[a-z]{2}\.html$/i)) { // no trailing slash return url.slice(0, -1 * ".en.html".length); } }, ]); t.is(tp2.toHref(), "/subdir/about"); }); ================================================ FILE: test/TemplateRenderCustomTest.js ================================================ import test from "ava"; import { createSSRApp } from "vue"; import { renderToString } from "@vue/server-renderer"; import * as sass from "sass"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; async function getNewTemplateRender(name, inputDir, eleventyConfig, extensionMap) { if (!eleventyConfig) { eleventyConfig = await getTemplateConfigInstance(); } if (!extensionMap) { extensionMap = new EleventyExtensionMap(eleventyConfig); extensionMap.setFormats([]); } let mgr = new TemplateEngineManager(eleventyConfig); extensionMap.engineManager = mgr; let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = extensionMap; await tr.init(); return tr; } test("Custom plaintext Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("txt", { compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); } ); let tr = await getNewTemplateRender("txt", null, eleventyConfig); let fn = await tr.getCompiledTemplate("

Paragraph

"); t.is(await fn(), "

Paragraph

"); t.is(await fn({}), "

Paragraph

"); }); test("Custom Markdown Render with `compile` override", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("md", { compile: function (str, inputPath) { return function (data) { return `${str.trim()}`; }; }, }); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate("# Markdown?"); t.is((await fn()).trim(), "# Markdown?"); t.is((await fn({})).trim(), "# Markdown?"); }); test("Custom Markdown Render without `compile` override", async (t) => { let initCalled = false; let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("md", { init: function () { initCalled = true; }, }); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate("# Header"); t.is(initCalled, true); t.is((await fn()).trim(), "

Header

"); t.is((await fn({})).trim(), "

Header

"); }); test("Custom Markdown Render with `compile` override + call to default compiler", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("md", { compile: function (str, inputPath) { return async function (data) { const result = await this.defaultRenderer(data); return `${result.trim()}`; }; }, }); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate("Hey {{name}}"); t.is((await fn()).trim(), "

Hey

"); t.is((await fn({ name: "Zach" })).trim(), "

Hey Zach

"); }); test("Custom Vue Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("vue", { compile: function (str) { return async function (data) { const app = createSSRApp({ template: str, data: function () { return data; }, }); return renderToString(app); }; }, }); } ); let tr = await getNewTemplateRender("vue", null, eleventyConfig); let fn = await tr.getCompiledTemplate('

Paragraph

'); t.is(await fn({ test: "Hello" }), "

Hello

"); }); test("Custom Sass Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("sass", { compile: function (str, inputPath) { // TODO declare data variables as SASS variables? return async function (data) { return new Promise(function (resolve, reject) { sass.render( { data: str, includePaths: [tr.inputDir, tr.includesDir], style: "expanded", indentType: "space", // TODO // sourcemap: "file", outFile: "test_this_is_to_not_write_a_file.css", }, function (error, result) { if (error) { reject(error); } else { resolve(result.css.toString("utf8")); } }, ); }); }; }, }); } ); let tr = await getNewTemplateRender("sass", null, eleventyConfig); let fn = await tr.getCompiledTemplate("$color: blue; p { color: $color; }"); t.is( (await fn({})).trim(), `p { color: blue; }`, ); }); /* serverPrefetch: function() { return this.getBlogAuthors().then(response => this.glossary = response) }, */ test("JavaScript functions should not be mutable but not *that* mutable", async (t) => { t.plan(3); let instance = { dataForCascade: function () { // was mutating this.config.javascriptFunctions! this.shouldnotmutatethething = 1; return {}; }, }; let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("js1", { getData: ["dataForCascade"], getInstanceFromInputPath: function (inputPath) { t.truthy(true); return instance; }, compile: function (str, inputPath) { t.falsy(this.config.javascriptFunctions.shouldnotmutatethething); // plaintext return (data) => { return str; }; }, }); } ); let tmpl = await getNewTemplate( "./test/stubs-custom-extension/test.js1", "./test/stubs-custom-extension/", "dist", null, null, eleventyConfig, ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "

Paragraph

"); }); test("Return undefined in compile to ignore #2267", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // addExtension() API cfg.addExtension("txt", { compile: function (str, inputPath) { return; }, }); } ); let tr = await getNewTemplateRender("txt", null, eleventyConfig); let fn = await tr.getCompiledTemplate("

Paragraph

"); t.is(fn, undefined); }); test("Simple alias to Markdown Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension("mdx", { key: "md", }); } ); let tr = await getNewTemplateRender("mdx", null, eleventyConfig); let fn = await tr.getCompiledTemplate("# Header"); t.is((await fn()).trim(), "

Header

"); t.is((await fn({})).trim(), "

Header

"); }); // NOTE: Breaking change in 3.0 `import` does not allow aliasing to non-.js file names test.skip("Breaking Change (3.0): Simple alias to JavaScript Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension("11ty.custom", { key: "11ty.js", }); } ); let tr = await getNewTemplateRender("./test/stubs/string.11ty.custom", null, eleventyConfig); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Bill" }), "

Zach

"); }); // NOTE: Breaking change in 3.0 `import` does not allow aliasing to non-.js file names test.skip("Breaking Change (3.0): Override to JavaScript Render", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension("11ty.custom", { key: "11ty.js", init: function () {}, }); } ); let tr = await getNewTemplateRender("./test/stubs/string.11ty.custom", null, eleventyConfig); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Bill" }), "

Zach

"); }); // NOTE: Breaking change in 3.0 `import` does not allow aliasing to non-.js file names test.skip("Breaking Change (3.0): Two simple aliases to JavaScript Render", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension(["11ty.custom", "11ty.possum"], { key: "11ty.js", // esm }); } ); let map = new EleventyExtensionMap(eleventyConfig); // reuse this map.setFormats([]); let tr = await getNewTemplateRender("./test/stubs/string.11ty.custom", null, eleventyConfig, map); let fn = await tr.getCompiledTemplate(); t.is(await fn({}), "

Zach

"); let tr2 = await getNewTemplateRender( "./test/stubs/string.11ty.possum", null, eleventyConfig, map, ); let fn2 = await tr2.getCompiledTemplate(); t.is(await fn2({}), "

Possum

"); }); test("Double override (one simple alias to custom) works fine", async (t) => { t.plan(3); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension(["11ty.possum"], { init: function () { t.true(true); }, compile: function(content, inputPath) { t.true(true); return (data) => "Appended " + content; } }); cfg.addExtension(["customhtml"], { key: "11ty.possum", }); } ); let map = new EleventyExtensionMap(eleventyConfig); // reuse this // map.setFormats(["11ty.possum", "11ty.custom"]); map.setFormats(["customhtml"]); let tr = await getNewTemplateRender( "customhtml", null, eleventyConfig, map, ); let fn = await tr.getCompiledTemplate("Template content"); t.is(await fn({}), "Appended Template content"); }); test("Double override (two simple aliases)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension(["11ty.possum"], { key: "html", }); cfg.addExtension(["customhtml"], { key: "11ty.possum", }); } ); let map = new EleventyExtensionMap(eleventyConfig); // reuse this // map.setFormats(["11ty.possum", "11ty.custom"]); map.setFormats(["customhtml"]); let tr = await getNewTemplateRender( "customhtml", null, eleventyConfig, map, ); let fn = await tr.getCompiledTemplate("Template content"); t.is(await fn({}), "Template content"); }); test("Double override (two complex aliases) is supported as of 3.0", async (t) => { t.plan(5); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension(["possum"], { key: "html", init: function () { t.true(true); }, compile: function() { t.true(true); return async function(data) { const result = await this.defaultRenderer(data); return `possum|${result}`; }; } }); cfg.addExtension(["11ty.custom"], { key: "possum", init: function () { t.true(true); }, compile: function() { t.true(true); return async function(data) { const result = await this.defaultRenderer(data); return `11ty.custom|${result}`; }; } }); } ); let map = new EleventyExtensionMap(eleventyConfig); // reuse this map.setFormats(["possum", "11ty.custom"]); let tr = await getNewTemplateRender( "11ty.custom", null, eleventyConfig, map, ); let fn = await tr.getCompiledTemplate("Template content"); t.is(await fn({}), "11ty.custom|possum|Template content"); }); test("Double override (not aliases) throws an error", async (t) => { await t.throwsAsync( async () => { await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addExtension(["md"], { compile: function (inputContent, inputPath) { return () => inputContent; }, }); cfg.addExtension(["md"], { compile: function (inputContent, inputPath) { return () => inputContent; }, }); } ); }, { message: 'An attempt was made to override the "md" template syntax twice (via the `addExtension` configuration API). A maximum of one override is currently supported.', }, ); }); ================================================ FILE: test/TemplateRenderHTMLTest.js ================================================ import test from "ava"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } // HTML test("HTML", async (t) => { let tr = await getNewTemplateRender("html"); t.is(tr.getEngineName(), "html"); }); test("HTML Render", async (t) => { let tr = await getNewTemplateRender("html"); let fn = await tr.getCompiledTemplate("

Paragraph

"); t.is(await fn(), "

Paragraph

"); t.is(await fn({}), "

Paragraph

"); }); test("HTML Render: Parses HTML using liquid engine (default, with data)", async (t) => { let tr = await getNewTemplateRender("html"); let fn = await tr.getCompiledTemplate("

{{title}}

"); t.is((await fn({ title: "My Title" })).trim(), "

My Title

"); }); test("HTML Render: Set HTML engine to false, don’t parse", async (t) => { let tr = await getNewTemplateRender("html"); tr.setHtmlEngine(false); let fn = await tr.getCompiledTemplate("

{{title}}

"); t.is((await fn()).trim(), "

{{title}}

"); }); test("HTML Render: Pass in an override (liquid)", async (t) => { let tr = await getNewTemplateRender("html"); tr.setHtmlEngine("liquid"); let fn = await tr.getCompiledTemplate("

{{title}}

"); t.is((await fn({ title: "My Title" })).trim(), "

My Title

"); }); ================================================ FILE: test/TemplateRenderJavaScriptTest.js ================================================ import test from "ava"; import TemplateRender from "../src/TemplateRender.js"; import Eleventy from "../src/Eleventy.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir, extendedConfig) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }, null, extendedConfig); eleventyConfig.setProjectUsingEsm(true); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } test("JS", async (t) => { t.is((await getNewTemplateRender("11ty.js")).getEngineName(), "11ty.js"); t.is((await getNewTemplateRender("./test/stubs/filename.11ty.js")).getEngineName(), "11ty.js"); t.is((await getNewTemplateRender("11ty.cjs")).getEngineName(), "11ty.js"); t.is((await getNewTemplateRender("./test/stubs/filename.11ty.cjs")).getEngineName(), "11ty.js"); }); test("JS Render a string (no data)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/string.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Bill" }), "

Zach

"); }); test("JS Render a promise (no data)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/promise.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Bill" }), "

Zach

"); }); test("JS Render a buffer (no data)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/buffer.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Bill" }), "

tést

"); }); test("JS Render a function", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

Zach

"); t.is(await fn({ name: "Bill" }), "

Bill

"); }); test("JS Render a function (arrow syntax)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-arrow.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

Zach

"); t.is(await fn({ name: "Bill" }), "

Bill

"); }); test("JS Render a function, returns a Buffer", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-buffer.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "tést" }), "

tést

"); t.is(await fn({ name: "Zach" }), "

Zach

"); t.is(await fn({ name: "Bill" }), "

Bill

"); }); test("JS Render a function (Markdown)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-markdown.11ty.cjs"); await tr.setEngineOverride("11ty.js,md"); let fn = await tr.getCompiledTemplate(); t.is((await fn({ name: "Zach" })).trim(), "

Zach

"); t.is((await fn({ name: "Bill" })).trim(), "

Bill

"); }); test("JS Render a function (Collections)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/use-collection.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is( ( await fn({ collections: { post: [ { data: { title: "Testing", }, }, { data: { title: "Testing2", }, }, ], }, }) ).trim(), `
  • Testing
  • Testing2
` ); }); test("JS Render an async function", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-async.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

Zach

"); t.is(await fn({ name: "Bill" }), "

Bill

"); }); test("JS Render with a Class", async (t) => { let tr = await getNewTemplateRender("./test/stubs/class.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

ZachBillTed

"); t.is(await fn({ name: "Bill" }), "

BillBillTed

"); }); test("JS Render with a Class, returns a buffer", async (t) => { let tr = await getNewTemplateRender("./test/stubs/class-buffer.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zách" }), "

ZáchBillTed

"); t.is(await fn({ name: "Zach" }), "

ZachBillTed

"); t.is(await fn({ name: "Bill" }), "

BillBillTed

"); }); test("JS Render with a Class, async render", async (t) => { let tr = await getNewTemplateRender("./test/stubs/class-async.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

Zach

"); t.is(await fn({ name: "Bill" }), "

Bill

"); }); test("JS Render using Vue", async (t) => { let tr = await getNewTemplateRender("./test/stubs/vue.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach" }), "

Hello Zach, this is a Vue template.

"); t.is(await fn({ name: "Bill" }), "

Hello Bill, this is a Vue template.

"); }); test("JS Render using Vue (with a layout)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/vue-layout.11ty.cjs"); let fn = await tr.getCompiledTemplate(); t.is( await fn({ name: "Zach" }), ` Test

Hello Zach, this is a Vue template.

` ); }); test("JS Render with a function", async (t) => { t.plan(8); let tr = await getNewTemplateRender("./test/stubs/function-filter.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { t.is(this.page.url, "/hi/"); // sanity check to make sure data didn’t propagate t.not(this.name, "Zach"); t.not(this.name, "Bill"); return new String(val).toUpperCase(); }, }, }); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach", page: { url: "/hi/" } }), "

ZACHT9000

"); t.is(await fn({ name: "Bill", page: { url: "/hi/" } }), "

BILLT9000

"); }); test("JS Render with a function and async filter", async (t) => { t.plan(4); let tr = await getNewTemplateRender("./test/stubs/function-async-filter.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { return new Promise((resolve) => { t.is(this.page.url, "/hi/"); resolve(new String(val).toUpperCase()); }); }, }, }); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach", page: { url: "/hi/" } }), "

ZACH

"); t.is(await fn({ name: "Bill", page: { url: "/hi/" } }), "

BILL

"); }); test("JS Render with a function prototype", async (t) => { t.plan(4); let tr = await getNewTemplateRender("./test/stubs/function-prototype.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { t.is(this.page.url, "/hi/"); return new String(val).toUpperCase(); }, }, }); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach", page: { url: "/hi/" } }), "

ZACHBillT9001

"); t.is(await fn({ name: "Bill", page: { url: "/hi/" } }), "

BILLBillT9001

"); }); test("JS Class Render with a function", async (t) => { t.plan(4); let tr = await getNewTemplateRender("./test/stubs/class-filter.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { t.is(this.page.url, "/hi/"); return new String(val).toUpperCase(); }, }, }); let fn = await tr.getCompiledTemplate(); t.is(await fn({ name: "Zach", page: { url: "/hi/" } }), "

ZACHBillTed

"); t.is(await fn({ name: "Bill", page: { url: "/hi/" } }), "

BILLBillTed

"); }); test("JS Class Async Render with a function", async (t) => { t.plan(4); let tr = await getNewTemplateRender("./test/stubs/class-async-filter.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { t.is(this.page.url, "/hi/"); return new String(val).toUpperCase(); }, }, }); let fn = await tr.getCompiledTemplate(); // Overrides all names to Ted t.is(await fn({ name: "Zach", page: { url: "/hi/" } }), "

ZACHBillTed

"); t.is(await fn({ name: "Bill", page: { url: "/hi/" } }), "

BILLBillTed

"); }); test("JS Class Async Render with a function (sync function, throws error)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-throws.11ty.cjs", undefined, { javascriptFunctions: { upper: function (val) { throw new Error("JS Class Async Render with a function (sync function, throws error)"); }, }, }); let error = await t.throwsAsync(async () => { let fn = await tr.getCompiledTemplate(); await fn({ name: "Zach" }); }); t.true( error.message.indexOf("JS Class Async Render with a function (sync function, throws error)") > -1 ); }); test("JS Class Async Render with a function (async function, throws error)", async (t) => { let tr = await getNewTemplateRender("./test/stubs/function-throws-async.11ty.cjs", undefined, { javascriptFunctions: { upper: async function (val) { throw new Error("JS Class Async Render with a function (async function, throws error)"); }, }, }); let error = await t.throwsAsync(async () => { let fn = await tr.getCompiledTemplate(); await fn({ name: "Zach" }); }); t.true( error.message.indexOf("JS Class Async Render with a function (async function, throws error)") > -1 ); }); test("JS function has access to built in filters", async (t) => { t.plan(6); let tr = await getNewTemplateRender("./test/stubs/function-fns.11ty.cjs"); let fn = await tr.getCompiledTemplate(); await fn({ avaTest: t, page: { url: "/hi/" } }); }); test("Class has access to built in filters", async (t) => { t.plan(6); let tr = await getNewTemplateRender("./test/stubs/class-fns.11ty.cjs"); let fn = await tr.getCompiledTemplate(); await fn({ avaTest: t, page: { url: "/hi/" } }); }); test("Class has page property already and keeps it", async (t) => { t.plan(2); let tr = await getNewTemplateRender("./test/stubs/class-fns-has-page.11ty.cjs"); let fn = await tr.getCompiledTemplate(); await fn({ avaTest: t, page: { url: "/hi/" } }); }); test("File has default function export and another one too, issue #3288", async (t) => { let tr = await getNewTemplateRender("./test/stubs/default-export-and-others.11ty.js"); let fn = await tr.getCompiledTemplate(); t.is(await fn(), "

hello

") }); test("File has default class export and another one too, issue #3359", async (t) => { let elev = new Eleventy("./test/stubs/default-class-export-and-others.11ty.js", ""); let results = await elev.toJSON(); t.is(results[0].content, "
hello
") }); test("File has default function export and another one for data too, issue #3359", async (t) => { let elev = new Eleventy("./test/stubs/default-function-export-and-named-data.11ty.js", ""); let results = await elev.toJSON(); t.is(results[0].content, "

Hello World

") }); test("File has default function export and another one for data too, issue #3359 (CommonJS)", async (t) => { let elev = new Eleventy("./test/stubs/default-function-export-and-named-data.11ty.cjs", ""); let results = await elev.toJSON(); t.is(results[0].content, "

Hello World

") }); ================================================ FILE: test/TemplateRenderLiquidTest.js ================================================ import test from "ava"; import { Liquid, Drop } from "liquidjs"; import Eleventy from "../src/Eleventy.js"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; async function getNewTemplateRender(name, inputDir, userConfig = {}) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }, null, userConfig); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } async function getPromise(resolveTo) { return new Promise(function (resolve) { setTimeout(function () { resolve(resolveTo); }); }); } // Liquid test("Liquid", async (t) => { let tr = await getNewTemplateRender("liquid"); t.is(tr.getEngineName(), "liquid"); }); test("Liquid Render Addition", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ number | plus: 1 }}

"); t.is(await fn({ number: 1 }), "

2

"); }); test("Liquid Render Raw", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{% raw %}{{name}}{% endraw %}

"); t.is(await fn({ name: "tim" }), "

{{name}}

"); }); test("Liquid Render Raw Multiline", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate( `

{% raw %} {{name}} {% endraw %}

` ); t.is( await fn({ name: "tim" }), `

{{name}}

` ); }); test("Liquid Render (with Helper)", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{name | capitalize}}

"); t.is(await fn({ name: "tim" }), "

Tim

"); }); test("Liquid Render Include", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr2.getCompiledTemplate("

{% include included %}

"); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Relative Include (dynamicPartials off)", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); // Important note: when inputPath is set to `liquid`, this *only* uses _includes relative paths in Liquid->compile let fn = await tr2.getCompiledTemplate("

{% include ./included %}

"); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Relative Include (dynamicPartials on)", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/"); // Important note: when inputPath is set to `liquid`, this *only* uses _includes relative paths in Liquid->compile let fn = await tr2.getCompiledTemplate("

{% include './included' %}

"); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Relative (current dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/relative-liquid/does_not_exist_and_thats_ok.liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, } ); let fn = await tr.getCompiledTemplate("

{% include ./dir/included %}

"); t.is(await fn(), "

TIME IS RELATIVE.

"); }); test("Liquid Render Relative (parent dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/relative-liquid/dir/does_not_exist_and_thats_ok.liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, } ); let fn = await tr.getCompiledTemplate("

{% include ../dir/included %}

"); t.is(await fn(), "

TIME IS RELATIVE.

"); }); test("Liquid Render Relative (relative include should ignore _includes dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/does_not_exist_and_thats_ok.liquid", "./test/stubs/", {} ); let fn = await tr.getCompiledTemplate(`

{% include './included' %}

`); t.is(await fn(), "

This is not in the includes dir.

"); }); test("Liquid Render Include with Liquid Suffix", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr2.getCompiledTemplate("

{% include included.liquid %}

"); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include with HTML Suffix", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr2.getCompiledTemplate("

{% include included.html %}

"); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include with HTML Suffix and Data Pass in", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr2.getCompiledTemplate("{% include included-data.html, myVariable: 'myValue' %}"); t.is((await fn()).trim(), "This is an include. myValue"); }); test("Liquid Custom Filter", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addFilter("prefixWithZach", function (val) { return "Zach" + val; }); t.is(await tr._testRender("{{ 'test' | prefixWithZach }}", {}), "Zachtest"); }); test("Liquid Async Filter", async (t) => { let tr = await getNewTemplateRender("liquid", "test/stubs"); tr.engine.addFilter("myAsyncFilter", async function (value) { return new Promise((resolve, reject) => { setTimeout(function () { resolve(`HI${value}`); }, 100); }); }); let fn = await tr.getCompiledTemplate("{{ 'test' | myAsyncFilter }}"); t.is((await fn()).trim(), "HItest"); }); test("Issue 3206: Strict variables and custom filters in includes", async (t) => { let tr = await getNewTemplateRender("liquid", "test/stubs", { liquidOptions: { strictVariables: true } }); tr.engine.addFilter("makeItFoo", function () { return "foo"; }); let fn = await tr.getCompiledTemplate(`

{% render "custom-filter", name: "Zach" %}

`); t.is((await fn()), "

foo

"); }); test("Liquid Custom Tag prefixWithZach", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addTag("prefixWithZach", function (liquidEngine) { return { parse: function (tagToken, remainTokens) { this.str = tagToken.args; // name }, render: function (ctx, hash) { var str = liquidEngine.evalValueSync(this.str, ctx.environments); // 'alice' return Promise.resolve("Zach" + str); // 'Alice' }, }; }); t.is(await tr._testRender("{% prefixWithZach name %}", { name: "test" }), "Zachtest"); }); test("Liquid Custom Tag postfixWithZach", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addTag("postfixWithZach", function (liquidEngine) { return { parse: function (tagToken, remainTokens) { this.str = tagToken.args; }, render: async function (ctx, hash) { var str = await liquidEngine.evalValue(this.str, ctx.environments); return Promise.resolve(str + "Zach"); }, }; }); t.is(await tr._testRender("{% postfixWithZach name %}", { name: "test" }), "testZach"); }); test("Liquid Custom Tag Unquoted String", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addTag("testUnquotedStringTag", function (liquidEngine) { return { parse: function (tagToken, remainTokens) { this.str = tagToken.args; }, render: function (scope, hash) { return Promise.resolve(this.str + "Zach"); }, }; }); t.is( await tr._testRender("{% testUnquotedStringTag _posts/2016-07-26-name-of-post.md %}", { name: "test", }), "_posts/2016-07-26-name-of-post.mdZach" ); }); test("Liquid addTag errors", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); t.throws(() => { tr.engine.addTag("badSecondArgument", {}); }); }); test("Liquid addTags", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addCustomTags({ postfixWithZach: function (liquidEngine) { return { parse: function (tagToken, remainTokens) { this.str = tagToken.args; }, render: async function (ctx, hash) { var str = await liquidEngine.evalValue(this.str, ctx.environments); return Promise.resolve(str + "Zach"); }, }; }, }); t.is(await tr._testRender("{% postfixWithZach name %}", { name: "test" }), "testZach"); }); test("Liquid Shortcode", async (t) => { t.plan(3); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { // Data in context t.is(this.page.url, "/hi/"); t.not(this.name, "test"); return str + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Liquid Shortcode returns promise", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { // Data in context t.is(this.page.url, "/hi/"); return new Promise(function (resolve) { setTimeout(function () { resolve(str + "Zach"); }); }); }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Liquid Shortcode returns promise (await inside)", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", async function (str) { // Data in context t.is(this.page.url, "/hi/"); return await getPromise(str + "Zach"); }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Liquid Shortcode returns promise (no await inside)", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", async function (str) { // Data in context t.is(this.page.url, "/hi/"); return getPromise(str + "Zach"); }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Liquid Shortcode Safe Output", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { // Data in context t.is(this.page.url, "/hi/"); return `${str}`; }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "test" ); }); test("Liquid Paired Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return str + content + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name %}Content{% endpostfixWithZach %}", { name: "test", page: { url: "/hi/", }, }), "testContentZach" ); }); test("Liquid Async Paired Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return new Promise(function (resolve) { setTimeout(function () { resolve(str + content + "Zach"); }); }); }); t.is( await tr._testRender("{% postfixWithZach name %}Content{% endpostfixWithZach %}", { name: "test", page: { url: "/hi/", }, }), "testContentZach" ); }); test("Liquid Render Include Subfolder", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included.liquid %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder HTML", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included.html %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder No file extension", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included %}

`); t.is(await fn(), "

This is an include.

"); }); // Related to https://github.com/harttle/liquidjs/issues/61 // Note that we swapped the dynamicPartials default in Eleventy 1.0 from false to true test("Liquid Render Include Subfolder Single quotes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included.liquid' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included.liquid" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes HTML", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included.html' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes HTML", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included.html" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes No file extension", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes No file extension", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included" %}

`); t.is(await fn(), "

This is an include.

"); }); /* End tests related to dynamicPartials */ test("Liquid Options Overrides", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { dynamicPartials: false, }, }); let options = tr.engine.getLiquidOptions(); t.is(options.dynamicPartials, false); }); test("Liquid Render Include Subfolder Single quotes no extension dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes (relative include current dir) dynamicPartials true", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/does_not_exist_and_thats_ok.liquid", "./test/stubs/", {} ); let fn = await tr.getCompiledTemplate(`

{% include './relative-liquid/dir/included' %}

`); t.is(await fn(), "

TIME IS RELATIVE.

"); }); test("Liquid Render Include Subfolder Single quotes (relative include parent dir) dynamicPartials true", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/subfolder/does_not_exist_and_thats_ok.liquid", "./test/stubs/", {} ); let fn = await tr.getCompiledTemplate(`

{% include '../relative-liquid/dir/included' %}

`); t.is(await fn(), "

TIME IS RELATIVE.

"); }); test("Liquid Render Include Subfolder Double quotes no extension dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included.liquid' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included.liquid" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes HTML dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include 'subfolder/included.html' %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes HTML dynamicPartials true", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included.html" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Single quotes HTML dynamicPartials true, data passed in", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate( `

{% include 'subfolder/included.html', myVariable: 'myValue' %}

` ); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder Double quotes HTML dynamicPartials true, data passed in", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr.getCompiledTemplate( `

{% include "subfolder/included.html", myVariable: "myValue" %}

` ); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render: with Library Override", async (t) => { const tr = await getNewTemplateRender("liquid"); tr.engine.setLibrary(new Liquid()); const fn = await tr.getCompiledTemplate("

{{name | capitalize}}

"); t.is(await fn({ name: "tim" }), "

Tim

"); }); test("Liquid Paired Shortcode with Tag Inside", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { return str + content + "Zach"; }); t.is( await tr._testRender( "{% postfixWithZach name %}Content{% if tester %}If{% endif %}{% endpostfixWithZach %}", { name: "test", tester: true } ), "testContentIfZach" ); }); test("Liquid Nested Paired Shortcode", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { return str + content + "Zach"; }); t.is( await tr._testRender( "{% postfixWithZach name %}Content{% postfixWithZach name2 %}Content{% endpostfixWithZach %}{% endpostfixWithZach %}", { name: "test", name2: "test2" } ), "testContenttest2ContentZachZach" ); }); test("Liquid Shortcode Multiple Args", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str, str2) { return str + str2 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name other %}", { name: "test", other: "howdy", }), "testhowdyZach" ); }); test("Liquid Include Scope Leak", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); // This is by design, `include` assigns value to its parent scope, // use `{% render %}` for separated, clean scope // see: https://github.com/harttle/liquidjs/issues/404#issuecomment-955660149 let tr2 = await getNewTemplateRender("liquid", "./test/stubs/"); let fn = await tr2.getCompiledTemplate("

{% include 'scopeleak' %}{{ test }}

"); t.is(await fn({ test: 1 }), "

22

"); }); test("Liquid Render Scope Leak", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); let tr2 = await getNewTemplateRender("liquid", "./test/stubs/"); // see scopeleak.liquid let fn = await tr2.getCompiledTemplate("

{% render 'scopeleak' %}-{{ test }}

"); t.is(await fn({ test: 1 }), "

2-1

"); }); // Note: this strictFilters default changed in 1.0 from false to true test("Liquid Missing Filter Issue #183 (no strictFilters)", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/", { liquidOptions: { strictFilters: false, }, }); try { await tr._testRender("{{ 'test' | prefixWithZach }}", {}); t.pass("Did not error."); } catch (e) { t.fail("Threw an error."); } }); // Note: this strictFilters default changed in 1.0 from false to true test("Liquid Missing Filter Issue #183", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); try { await tr._testRender("{{ 'test' | prefixWithZach }}", {}); t.fail("Did not error."); } catch (e) { t.pass("Threw an error."); } }); test("Issue 258: Liquid Render Date", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ myDate }}

"); let dateStr = await fn({ myDate: new Date(Date.UTC(2016, 0, 1, 0, 0, 0)) }); t.is(dateStr.slice(0, 3), "

"); t.is(dateStr.slice(-4), "

"); t.not(dateStr.slice(2, 1), '"'); }); test("Issue 347: Liquid addTags with space in argument", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addCustomTags({ issue347CustomTag: function (liquidEngine) { return { parse: function (tagToken, remainTokens) { this.str = tagToken.args; }, render: async function (scope, hash) { var str = await liquidEngine.evalValue(this.str, scope); return Promise.resolve(str + "Zach"); }, }; }, }); t.is( await tr._testRender("{% issue347CustomTag 'te st' %}", { name: "slkdjflksdjf", }), "te stZach" ); }); test("Issue 347: Liquid Shortcode, string argument", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str) { return str + "Zach"; }); t.is(await tr._testRender("{% issue347 'test' %}", { name: "alkdsjfkslja" }), "testZach"); }); test("Issue 347: Liquid Shortcode string argument with space, double quotes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347b", function (str) { return str + "Zach"; }); t.is( await tr._testRender('{% issue347b "test 2" "test 3" %}', { name: "alkdsjfkslja", }), "test 2Zach" ); }); test("Issue 347: Liquid Shortcode string argument with space, single quotes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str) { return str + "Zach"; }); t.is(await tr._testRender("{% issue347 'test 2' %}", { name: "alkdsjfkslja" }), "test 2Zach"); }); test("Issue 347: Liquid Shortcode string argument with space, combination of quotes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str, str2) { return str + str2 + "Zach"; }); t.is( await tr._testRender("{% issue347 'test 2' \"test 3\" %}", { name: "alkdsjfkslja", }), "test 2test 3Zach" ); }); test("Issue 347: Liquid Shortcode multiple arguments, comma separated", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str, str2) { return str + str2 + "Zach"; }); t.is( await tr._testRender("{% issue347 'test 2', \"test 3\" %}", { name: "alkdsjfkslja", }), "test 2test 3Zach" ); }); test("Issue 347: Liquid Shortcode multiple arguments, comma separated, one is an integer", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str, str2) { return str + str2 + "Zach"; }); t.is( await tr._testRender("{% issue347 'test 2', 3 %}", { name: "alkdsjfkslja", }), "test 23Zach" ); }); test("Issue 347: Liquid Shortcode multiple arguments, comma separated, one is a float", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (str, str2) { return str + str2 + "Zach"; }); t.is( await tr._testRender("{% issue347 'test 2', 3.23 %}", { name: "alkdsjfkslja", }), "test 23.23Zach" ); }); test("Issue 347: Liquid Shortcode boolean argument", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue347", function (bool) { return bool ? "Zach" : "Not Zach"; }); t.is(await tr._testRender("{% issue347 true %}", { name: "alkdsjfkslja" }), "Zach"); t.is(await tr._testRender("{% issue347 false %}", { name: "alkdsjfkslja" }), "Not Zach"); }); test("Issue 347: Liquid Paired Shortcode with Spaces", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str1, num, str2) { return str1 + num + str2 + content + "Zach"; }); t.is( await tr._testRender( "{% postfixWithZach 'My Name', 1234, \"Other\" %}Content{% endpostfixWithZach %}", { name: "test" } ), "My Name1234OtherContentZach" ); }); test("Liquid Render with dash variable Issue #567", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ my-global-name }}

"); t.is(await fn({ "my-global-name": "Zach" }), "

Zach

"); }); test("Issue 600: Liquid Shortcode argument page.url", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue600", function (str) { return str + "Zach"; }); t.is( await tr._testRender("{% issue600 page.url %}", { page: { url: "alkdsjfkslja" }, }), "alkdsjfksljaZach" ); }); test("Issue 600: Liquid Shortcode argument with dashes", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue600b", function (str) { return str + "Zach"; }); t.is( await tr._testRender("{% issue600b page-url %}", { "page-url": "alkdsjfkslja", }), "alkdsjfksljaZach" ); }); test("Issue 600: Liquid Shortcode argument with underscores", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("issue600c", function (str) { return str + "Zach"; }); t.is( await tr._testRender("{% issue600c page_url %}", { page_url: "alkdsjfkslja", }), "alkdsjfksljaZach" ); }); test("Issue 611: Run a function", async (t) => { // function calls in Nunjucks can be replaced by custom Drops let tr = await getNewTemplateRender("liquid", "./test/stubs/"); class CustomDrop extends Drop { valueOf() { return "alkdsjfksljaZach"; } } t.is( await tr._testRender("{{ test }}", { test: new CustomDrop(), }), "alkdsjfksljaZach" ); }); test("Liquid Shortcode (with sync function, error throwing)", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { throw new Error("Liquid Shortcode (with sync function, error throwing)"); }); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZach name %}", { name: "test" }); }); t.true(error.message.indexOf("Liquid Shortcode (with sync function, error throwing)") > -1); }); test("Liquid Shortcode (with async function, error throwing)", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", async function (str) { throw new Error("Liquid Shortcode (with async function, error throwing)"); }); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZach name %}", { name: "test" }); }); t.true(error.message.indexOf("Liquid Shortcode (with async function, error throwing)") > -1); }); test("Liquid Render a false #1069", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("{{ falseValue }}"); t.is(await fn({ falseValue: false }), "false"); }); test("Liquid Render Square Brackets #680 dash single quotes", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ test['hey-a'] }}

"); t.is(await fn({ test: { "hey-a": 1 } }), "

1

"); }); test("Liquid Render Square Brackets #680 dash single quotes spaces", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ test[ 'hey-a' ] }}

"); t.is(await fn({ test: { "hey-a": 1 } }), "

1

"); }); test("Liquid Render Square Brackets #680 dash double quotes", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate('

{{ test["hey-a"] }}

'); t.is(await fn({ test: { "hey-a": 1 } }), "

1

"); }); test("Liquid Render Square Brackets #680 dash double quotes spaces", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate('

{{ test[ "hey-a" ] }}

'); t.is(await fn({ test: { "hey-a": 1 } }), "

1

"); }); test("Liquid Render Square Brackets #680 variable reference", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ test[ref] }}

"); t.is(await fn({ test: { "hey-a": 1 }, ref: "hey-a" }), "

1

"); }); test("Liquid Render Square Brackets #680 variable reference array", async (t) => { let tr = await getNewTemplateRender("liquid"); let fn = await tr.getCompiledTemplate("

{{ test[ref[0]] }}

"); t.is(await fn({ test: { "hey-a": 1 }, ref: ["hey-a"] }), "

1

"); }); test("Liquid bypass compilation", async (t) => { let tr = await getNewTemplateRender("liquid"); t.is(tr.engine.needsCompilation("

{{ me }}

"), true); t.is(tr.engine.needsCompilation("

{% comment %}{% endcomment %}

"), true); t.is(tr.engine.needsCompilation("

test

"), false); }); test("Liquid reverse filter in {{ }}", async (t) => { let tr = await getNewTemplateRender("liquid"); // https://liquidjs.com/filters/reverse.html let fn = await tr.getCompiledTemplate("{{ test | reverse | join: ',' }}"); t.is(await fn({ test: [1, 2, 3] }), "3,2,1"); }); test("Liquid reverse filter in {% for %}", async (t) => { let tr = await getNewTemplateRender("liquid"); // https://liquidjs.com/tags/for.html#reversed let fn = await tr.getCompiledTemplate("{% for num in test reversed %}{{ num }}{% endfor %}"); t.is(await fn({ test: [1, 2, 3] }), "321"); }); test("Liquid Parse for Symbols", async (t) => { let tr = await getNewTemplateRender("liquid"); let engine = tr.engine; t.deepEqual(engine.parseForSymbols("

{{ name }}

"), ["name"]); t.deepEqual(engine.parseForSymbols("

{{ eleventy.deep.nested }}

"), [ "eleventy.deep.nested", ]); t.deepEqual(engine.parseForSymbols("

{{ a }} {{ b }}

"), ["a", "b"]); t.deepEqual(engine.parseForSymbols("

{% if true %}{{ c }}{% endif %}

"), ["c"]); t.deepEqual(engine.parseForSymbols("

{% if false %}{{ c }}{% endif %}

"), ["c"]); t.deepEqual(engine.parseForSymbols("{{ collections.all[0] }}>"), [ // Note that the Nunjucks parser returns collections.all "collections.all[0]", ]); t.deepEqual(engine.parseForSymbols("{{ collections.mine }}>"), ["collections.mine"]); t.deepEqual(engine.parseForSymbols("{{ collections.mine | test }}>"), ["collections.mine"]); }); test("Eleventy shortcode uses new built-in Liquid argument parsing behavior (spaces)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc def %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, "[123,456]"); }); test("Eleventy shortcode uses new built-in Liquid argument parsing behavior (commas)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc, def %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, "[123,456]"); }); test("Eleventy shortcode uses new built-in Liquid argument parsing behavior (commas, no spaces)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc,def %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, "[123,456]"); }); test("Eleventy paired shortcode uses new built-in Liquid argument parsing behavior (spaces)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addPairedShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc def %}hi{% endtest %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, `["hi",123,456]`); }); test("Eleventy paired shortcode uses new built-in Liquid argument parsing behavior (commas)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addPairedShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc, def %}hi{% endtest %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, `["hi",123,456]`); }); test("Eleventy paired shortcode uses new built-in Liquid argument parsing behavior (commas, no spaces)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setLiquidParameterParsing("builtin"); eleventyConfig.addPairedShortcode("test", (...args) => { return JSON.stringify(args); }) eleventyConfig.addTemplate("index.liquid", `{% test abc,def %}hi{% endtest %}`, { abc: 123, def: 456 }); } }); elev.disableLogger(); let [result] = await elev.toJSON(); t.deepEqual(result.content, `["hi",123,456]`); }); test("jsTruthy default changed, breaking in v4 #3507", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.liquid", `{% if emptyString %}notempty{% endif %}-{% if zero %}notzero{% endif %}-{% if not emptyString %}empty{% endif %}-{% if not zero %}zero{% endif %}`, { emptyString: "", zero: 0 }); } }); let [result] = await elev.toJSON(); t.deepEqual(result.content, `--empty-zero`); }); test("Use globals for page/eleventy/collections for use inside {% render %} #1541", async (t) => { let elev = new Eleventy("./test/stubs/stubs-1541/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.liquid", `{% render "render-source.liquid" %}`); } }); let [result] = await elev.toJSON(); t.deepEqual(result.content, `/ via script collections.all size: 1`); }); ================================================ FILE: test/TemplateRenderMarkdownPluginTest.js ================================================ import test from "ava"; import md from "markdown-it"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir) { let eleventyConfig = await getTemplateConfigInstance(); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } const createTestMarkdownPlugin = () => { const plugin = (md) => { md.core.ruler.after("inline", "replace-link", function (state) { plugin.environment = state.env; const link = state.tokens[1].children[0].attrs[0][1]; state.tokens[1].children[0].attrs[0][1] = `${link}?data=${state.env.some}`; return false; }); }; plugin.environment = {}; return plugin; }; test("Markdown Render: with HTML prerender, sends context data to the markdown library", async (t) => { let tr = await getNewTemplateRender("md"); const plugin = createTestMarkdownPlugin(); let mdLib = md().use(plugin); tr.engine.setLibrary(mdLib); const data = { some: "data" }; let fn = await tr.getCompiledTemplate("[link text](http://link.com)"); let result = await fn(data); t.deepEqual(plugin.environment, data); t.is(result, '

link text

\n'); }); test("Markdown Render: without HTML prerender, sends context data to the markdown library", async (t) => { let tr = await getNewTemplateRender("md"); const plugin = createTestMarkdownPlugin(); let mdLib = md().use(plugin); tr.engine.setLibrary(mdLib); tr.setHtmlEngine(false); const data = { some: "data" }; let fn = await tr.getCompiledTemplate("[link text](http://link.com)"); let result = await fn(data); t.deepEqual(plugin.environment, data); t.is(result, '

link text

\n'); }); test("Markdown Render: renderer that only implements the render function", async (t) => { let tr = await getNewTemplateRender("md"); tr.engine.setLibrary({ render: (content) => { const [_, text, href] = content.match(/\[(.*)\]\((.*)\)/); return `

${text}

\n`; }, }); tr.setHtmlEngine(false); let fn = await tr.getCompiledTemplate("[link text](http://link.com)"); let result = await fn(); t.is(result, '

link text

\n'); }); ================================================ FILE: test/TemplateRenderMarkdownTest.js ================================================ import test from "ava"; import md from "markdown-it"; import { full as mdEmoji } from 'markdown-it-emoji' import eleventySyntaxHighlightPlugin from "@11ty/eleventy-plugin-syntaxhighlight"; import TemplateRender from "../src/TemplateRender.js"; import Liquid from "../src/Engines/Liquid.js"; import Nunjucks from "../src/Engines/Nunjucks.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir, eleventyConfig) { if (!eleventyConfig) { eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); } let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } // Markdown test("Markdown", async (t) => { let tr = await getNewTemplateRender("md"); t.is(tr.getEngineName(), "md"); }); test("Markdown Render: Parses base markdown, no data", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate("# My Title"); t.is((await fn()).trim(), "

My Title

"); }); test("Markdown Render: Markdown should work with HTML too", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate("

My Title

"); t.is((await fn()).trim(), "

My Title

"); }); test("Markdown Render: Parses markdown using liquid engine (default, with data)", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate("# {{title}}"); t.is((await fn({ title: "My Title" })).trim(), "

My Title

"); }); test("Markdown Render: Ignore markdown, use only preprocess engine (useful for variable resolution in permalinks)", async (t) => { let tr = await getNewTemplateRender("md"); tr.setUseMarkdown(false); let fn = await tr.getCompiledTemplate("{{title}}"); t.is((await fn({ title: "My Title" })).trim(), "My Title"); }); test("Markdown Render: Skip markdown and preprocess engine (issue #466)", async (t) => { let tr = await getNewTemplateRender("md"); tr.setMarkdownEngine(false); tr.setUseMarkdown(false); let fn = await tr.getCompiledTemplate("404.html"); t.is((await fn({ title: "My Title" })).trim(), "404.html"); }); test("Markdown Render: Set markdown engine to false, don’t parse", async (t) => { let tr = await getNewTemplateRender("md"); tr.setMarkdownEngine(false); let fn = await tr.getCompiledTemplate("# {{title}}"); t.is((await fn()).trim(), "

{{title}}

"); }); test("Markdown Render: Set markdown engine to false, don’t parse (test with HTML input)", async (t) => { let tr = await getNewTemplateRender("md"); tr.setMarkdownEngine(false); let fn = await tr.getCompiledTemplate("

{{title}}

"); t.is((await fn()).trim(), "

{{title}}

"); }); test("Markdown Render: Pass in an override (liquid)", async (t) => { let tr = await getNewTemplateRender("md"); tr.setMarkdownEngine("liquid"); let fn = await tr.getCompiledTemplate("# {{title}}"); t.is((await fn({ title: "My Title" })).trim(), "

My Title

"); }); test("Markdown Render: Strikethrough", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate("~~No~~"); t.is((await fn()).trim(), "

No

"); }); test("Markdown Render: Strikethrough in a Header", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate("# ~~No~~"); t.is((await fn()).trim(), "

No

"); }); test("Markdown Render: with Library Override", async (t) => { let tr = await getNewTemplateRender("md"); let mdLib = md(); tr.engine.setLibrary(mdLib); t.is(mdLib.render(":)").trim(), "

:)

"); let fn = await tr.getCompiledTemplate(":)"); t.is((await fn()).trim(), "

:)

"); }); test("Markdown Render: with Library Override and a Plugin", async (t) => { let tr = await getNewTemplateRender("md"); let mdLib = md().use(mdEmoji); tr.engine.setLibrary(mdLib); t.is(mdLib.render(":)").trim(), "

😃

"); let fn = await tr.getCompiledTemplate(":)"); t.is((await fn()).trim(), "

😃

"); }); test("Markdown Render: use a custom highlighter", async (t) => { let tr = await getNewTemplateRender("md"); let mdLib = md(); mdLib.set({ highlight: function (str, lang) { return "This is overrrrrrride"; }, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\` This is some code. \`\`\``); t.is((await fn()).trim(), "
This is overrrrrrride
"); }); test("Markdown Render: use prism highlighter (no language)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addPlugin(eleventySyntaxHighlightPlugin); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let markdownHighlight = eleventyConfig.getConfig().markdownHighlighter; let mdLib = md(); mdLib.set({ highlight: markdownHighlight, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\` This is some code. \`\`\``); t.is( (await fn()).trim(), `
This is some code.
` ); }); test("Markdown Render: use prism highlighter", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addPlugin(eleventySyntaxHighlightPlugin); } ); let tr = await getNewTemplateRender("md"); let markdownHighlight = eleventyConfig.getConfig().markdownHighlighter; let mdLib = md(); mdLib.set({ highlight: markdownHighlight, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\` js var key = "value"; \`\`\``); t.is( (await fn()).trim(), `
var key = "value";
` ); }); test("Markdown Render: use prism highlighter (no space before language)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addPlugin(eleventySyntaxHighlightPlugin); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let markdownHighlight = eleventyConfig.getConfig().markdownHighlighter; let mdLib = md(); mdLib.set({ highlight: markdownHighlight, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\`js var key = "value"; \`\`\``); t.is( (await fn()).trim(), `
var key = "value";
` ); }); test("Markdown Render: use prism highlighter, line highlighting", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addPlugin(eleventySyntaxHighlightPlugin); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let markdownHighlight = eleventyConfig.getConfig().markdownHighlighter; let mdLib = md(); mdLib.set({ highlight: markdownHighlight, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\`js/0 var key = "value"; \`\`\``); t.is( (await fn()).trim(), `
var key = "value";
` ); }); test("Markdown Render: use prism highlighter, line highlighting with fallback `text` language.", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addPlugin(eleventySyntaxHighlightPlugin); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let cfg = eleventyConfig.getConfig(); let markdownHighlight = cfg.markdownHighlighter; let mdLib = md(); mdLib.set({ highlight: markdownHighlight, }); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(`\`\`\` text/0 var key = "value"; \`\`\``); t.is( (await fn()).trim(), `
var key = "value";
` ); }); test("Markdown Render: use Markdown inside of a Liquid shortcode (Issue #536)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tr = await getNewTemplateRender("md", null, eleventyConfig); let liquidEngine = new Liquid("liquid", eleventyConfig); liquidEngine.addShortcode("testShortcode", function () { return "## My Other Title"; }); tr.setMarkdownEngine(liquidEngine); let fn = await tr.getCompiledTemplate(`# {{title}} {% testShortcode %}`); t.is( ( await fn({ title: "My Title", otherTitle: "My Other Title", }) ).trim(), `

My Title

My Other Title

` ); }); test("Markdown Render: use Markdown inside of a Nunjucks shortcode (Issue #536)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tr = await getNewTemplateRender("md", null, eleventyConfig); let nunjucksEngine = new Nunjucks("njk", eleventyConfig); nunjucksEngine.addShortcode("testShortcode", function () { return "## My Other Title"; }); tr.setMarkdownEngine(nunjucksEngine); let fn = await tr.getCompiledTemplate(`# {{title}} {% testShortcode %}`); t.is( ( await fn({ title: "My Title", otherTitle: "My Other Title", }) ).trim(), `

My Title

My Other Title

` ); }); test("Markdown Render: use Markdown inside of a Liquid paired shortcode (Issue #536)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tr = await getNewTemplateRender("md", null, eleventyConfig); let liquidEngine = new Liquid("liquid", eleventyConfig); liquidEngine.addPairedShortcode("testShortcode", function (content) { return content; }); tr.setMarkdownEngine(liquidEngine); let fn = await tr.getCompiledTemplate(`# {{title}} {% testShortcode %}## My Other Title{% endtestShortcode %}`); t.is( ( await fn({ title: "My Title", otherTitle: "My Other Title", }) ).trim(), `

My Title

My Other Title

` ); }); test("Markdown Render: use Markdown inside of a Nunjucks paired shortcode (Issue #536)", async (t) => { let eleventyConfig = await getTemplateConfigInstance(); let tr = await getNewTemplateRender("md", null, eleventyConfig); let nunjucksEngine = new Nunjucks("njk", eleventyConfig); nunjucksEngine.addPairedShortcode("testShortcode", function (content) { return content; }); tr.setMarkdownEngine(nunjucksEngine); let fn = await tr.getCompiledTemplate(`# {{title}} {% testShortcode %}## My Other Title{% endtestShortcode %}`); t.is( ( await fn({ title: "My Title", otherTitle: "My Other Title", }) ).trim(), `

My Title

My Other Title

` ); }); test("Markdown Render: Disable indented code blocks by default. Issue #2438", async (t) => { let tr = await getNewTemplateRender("md"); let fn = await tr.getCompiledTemplate(" This is a test"); t.is((await fn()).trim(), "

This is a test

"); }); test("Markdown Render: setLibrary does not have disabled indented code blocks either. Issue #2438", async (t) => { let tr = await getNewTemplateRender("md"); let mdLib = md(); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(" This is a test"); let content = await fn(); t.is(content.trim(), "

This is a test

"); }); test("Markdown Render: use amendLibrary to re-enable indented code blocks. Issue #2438", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.amendLibrary("md", (lib) => lib.enable("code")); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate(" This is a test"); let content = await fn(); t.is( normalizeNewLines(content.trim()), `
This is a test
` ); }); test("Markdown Render: amendLibrary works with setLibrary to re-enable indented code blocks. Issue #2438", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.amendLibrary("md", (lib) => lib.enable("code")); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let mdLib = md(); tr.engine.setLibrary(mdLib); let fn = await tr.getCompiledTemplate(" This is a test"); let content = await fn(); t.is( normalizeNewLines(content.trim()), `
This is a test
` ); }); test("Markdown Render: multiple amendLibrary calls. Issue #2438", async (t) => { t.plan(3); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.amendLibrary("md", (lib) => { t.true(true); lib.enable("code"); }); cfg.amendLibrary("md", (lib) => { t.true(true); lib.disable("code"); }); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate(" This is a test"); let content = await fn(); t.is(normalizeNewLines(content.trim()), "

This is a test

"); }); test("Markdown Render: use amendLibrary to add a Plugin", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.amendLibrary("md", (mdLib) => mdLib.use(mdEmoji)); } ); let tr = await getNewTemplateRender("md", null, eleventyConfig); let fn = await tr.getCompiledTemplate(":)"); t.is((await fn()).trim(), "

😃

"); }); ================================================ FILE: test/TemplateRenderNunjucksTest.js ================================================ import test from "ava"; import Nunjucks from "@11ty/nunjucks"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir, eleventyConfig) { if (!eleventyConfig) { eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); } let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } async function getPromise(resolveTo) { return new Promise(function (resolve) { setTimeout(function () { resolve(resolveTo); }); }); } // Nunjucks test("Nunjucks", async (t) => { let tr = await getNewTemplateRender("njk"); t.is(tr.getEngineName(), "njk"); }); test("Nunjucks Render", async (t) => { let tr = await getNewTemplateRender("njk"); let fn = await tr.getCompiledTemplate("

{{ name }}

"); t.is(await fn({ name: "Zach" }), "

Zach

"); }); test("Nunjucks Render Addition", async (t) => { let tr = await getNewTemplateRender("njk"); let fn = await tr.getCompiledTemplate("

{{ number + 1 }}

"); t.is(await fn({ number: 1 }), "

2

"); }); test("Nunjucks Render Extends", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate( "{% extends 'base.njk' %}{% block content %}This is a child.{% endblock %}" ); t.is(await fn(), "

This is a child.

"); }); test("Nunjucks Render Relative Extends", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "test/stubs" ); let fn = await tr.getCompiledTemplate( "{% extends '../dir/base.njk' %}{% block content %}This is a child.{% endblock %}" ); t.is(await fn(), "

This is a child.

"); }); test("Nunjucks Render Include", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate("

{% include 'included.njk' %}

"); t.is(await fn(), "

This is an include.

"); }); test("Nunjucks Render Include (different extension)", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate("

{% include 'included.nunj' %}

"); t.is(await fn(), "

Nunjabusiness

"); }); test("Nunjucks Render Include (different extension, subdir)", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate("

{% include 'subfolder/included.nunj' %}

"); t.is(await fn(), "

Nunjabusiness2

"); }); test("Nunjucks Render Relative Include Issue #190", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/does_not_exist_and_thats_ok.njk", "./test/stubs" ); let fn = await tr.getCompiledTemplate("

{% include './dir/included.njk' %}

"); t.is(await fn(), "

HELLO FROM THE OTHER SIDE.

"); }); test("Nunjucks Render Relative Include (using ..) Issue #190", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "./test/stubs" ); let fn = await tr.getCompiledTemplate("

{% include '../dir/included.njk' %}

"); t.is(await fn(), "

HELLO FROM THE OTHER SIDE.

"); // should look in _includes too, related to Issue #633 let fn2a = await tr.getCompiledTemplate("

{% include 'included-relative.njk' %}

"); t.is(await fn2a(), "

akdlsjafkljdskl

"); // should look in _includes too Issue #633 // let fn3 = await tr.getCompiledTemplate( // "

{% include '../_includes/included-relative.njk' %}

" // ); // t.is(await fn3(), "

akdlsjafkljdskl

"); }); test("Nunjucks Render Relative Include (using current dir) Issue #190", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "./test/stubs" ); let fn = await tr.getCompiledTemplate("

{% include './included.njk' %}

"); t.is(await fn(), "

HELLO FROM THE OTHER SIDE.

"); // This fails because ./ doesn’t look in _includes (this is good) // let fn = await tr.getCompiledTemplate( // "

{% include './included-relative.njk' %}

" // ); // t.is(await fn(), "

akdlsjafkljdskl

"); }); test("Nunjucks Render Relative Include (ambiguous path, file exists in _includes and in current dir) Issue #190", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "./test/stubs" ); let fn = await tr.getCompiledTemplate( // should prefer to use _includes first // more specifically, this will not use the current dir at all. "

{% include 'included.njk' %}

" ); t.is(await fn(), "

This is an include.

"); // This fails, a leading dot is required for a relative include // let tr2 = getNewTemplateRender("./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "./test/stubs"); // let fn2 = await tr.getCompiledTemplate( // "

{% include 'unique-include-123.njk' %}

" // ); // t.is(await fn2(), "

HELLO FROM THE OTHER SIDE.

"); }); test("Nunjucks Async Filter", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let engine = tr.engine; engine.addFilters( { myAsyncFilter: function (value, callback) { setTimeout(function () { callback(null, `HI${value}`); }, 100); }, }, true ); let fn = await tr.getCompiledTemplate("{{ 'test' | myAsyncFilter }}"); t.is((await fn()).trim(), "HItest"); }); test("Nunjucks Render set with a filter", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let engine = tr.engine; engine.addFilters({ uppercase: function (str) { return str.toUpperCase(); }, }); let fn = await tr.getCompiledTemplate(`{% set test = "hi" | uppercase %}{{ test }}`); t.is((await fn()).trim(), `HI`); }); test("Nunjucks Render Include a JS file (Issue 398)", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let engine = tr.engine; engine.addFilters({ jsmin: function (str) { return str; }, }); let fn = await tr.getCompiledTemplate( "{% set ga %}{% include 'test.js' %}{% endset %}{{ ga | safe | jsmin }}" ); t.is((await fn()).trim(), `/* THIS IS A COMMENT */ alert("Issue #398");`); }); test("Nunjucks Render Include Subfolder", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate("

{% include 'subfolder/included.html' %}

"); t.is(await fn(), "

This is an include.

"); }); test("Nunjucks Render Include Double Quotes", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate(`

{% include "included.njk" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Nunjucks Render Include Subfolder Double Quotes", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate(`

{% include "subfolder/included.html" %}

`); t.is(await fn(), "

This is an include.

"); }); test("Nunjucks Render Imports", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate( "{% import 'imports.njk' as forms %}
{{ forms.label('Name') }}
" ); t.is(await fn(), "
"); }); test("Nunjucks Render Relative Imports", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/njk-relative/dir/does_not_exist_and_thats_ok.njk", "test/stubs" ); let fn = await tr.getCompiledTemplate( "{% import '../dir/imports.njk' as forms %}
{{ forms.label('Name') }}
" ); t.is(await fn(), "
"); }); test("Nunjucks Render Imports From", async (t) => { let tr = await getNewTemplateRender("njk", "test/stubs"); let fn = await tr.getCompiledTemplate( "{% from 'imports.njk' import label %}
{{ label('Name') }}
" ); t.is(await fn(), "
"); }); test("Nunjucks getEngineLib", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); t.truthy(tr.engine.getEngineLib()); }); test("Nunjucks Render: with Library Override", async (t) => { let tr = await getNewTemplateRender("njk"); let env = new Nunjucks.Environment(new Nunjucks.FileSystemLoader("./test/stubs/_includes/")); tr.engine.setLibrary(env); let fn = await tr.getCompiledTemplate("

{{ name }}

"); t.is(await fn({ name: "Zach" }), "

Zach

"); }); test("Nunjucks Render with getGlobals Issue #567", async (t) => { let tr = await getNewTemplateRender("njk"); let env = tr.engine.getEngineLib(); env.addGlobal("getGlobals", function () { return this.getVariables(); }); let fn = await tr.getCompiledTemplate("

{{ getGlobals()['my-global-name'] }}

"); t.is(await fn({ "my-global-name": "Zach" }), "

Zach

"); }); test("Nunjucks Render with getVarFromString Filter Issue #567", async (t) => { let tr = await getNewTemplateRender("njk"); let env = tr.engine.getEngineLib(); env.addFilter("getVarFromString", function (varName) { return this.getVariables()[varName]; }); let fn = await tr.getCompiledTemplate("

{{ 'my-global-name' | getVarFromString }}

"); t.is(await fn({ "my-global-name": "Zach" }), "

Zach

"); }); test("Nunjucks Shortcode without args #372", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (arg1) { return arg1 + "Zach"; }); t.is(await tr._testRender("{% postfixWithZach %}", {}), "undefinedZach"); }); test("Nunjucks Shortcode", async (t) => { t.plan(3); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { // Data in context t.is(this.page.url, "/hi/"); // sanity check that all data is not carried forward t.not(this.name, "test"); return str + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Nunjucks Async Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode( "postfixWithZach", async function (str) { // Data in context t.is(this.page.url, "/hi/"); return new Promise(function (resolve) { setTimeout(function () { resolve(str + "Zach"); }); }); }, true ); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Nunjucks Async function Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode( "postfixWithZach", async function (str) { // Data in context t.is(this.page.url, "/hi/"); return await getPromise(str + "Zach"); }, true ); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "testZach" ); }); test("Nunjucks sync function Shortcode (error throwing)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode( "postfixWithZach", function (str) { throw new Error("Nunjucks sync function Shortcode (error throwing)"); }, false ); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZach name %}", { name: "test" }); }); t.true( error.message.indexOf( "EleventyNunjucksError: Error with Nunjucks shortcode `postfixWithZach`" ) > -1 ); t.true( error.cause.message.startsWith( "Error with Nunjucks shortcode `postfixWithZach`" ) ); t.true( error.cause.originalError.message.startsWith( "Nunjucks sync function Shortcode (error throwing)" ) ); }); test("Nunjucks Async function Shortcode (error throwing)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode( "postfixWithZachError", async function (str) { throw new Error("Nunjucks Async function Shortcode (error throwing)"); }, true ); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZachError name %}", { name: "test" }); }); t.true( error.message.indexOf( "EleventyNunjucksError: Error with Nunjucks shortcode `postfixWithZachError`" ) > -1 ); t.true( error.cause.message.startsWith( "Error with Nunjucks shortcode `postfixWithZachError`" ) ); t.true( error.cause.originalError.message.startsWith( "Nunjucks Async function Shortcode (error throwing)" ) ); }); test("Nunjucks sync function paired Shortcode (error throwing)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode( "postfixWithZachError", function (str) { throw new Error( "Nunjucks sync function paired Shortcode (error throwing)" ); }, false ); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZachError name %}hi{% endpostfixWithZachError %}", { name: "test", }); }); t.true( error.message.indexOf( "EleventyNunjucksError: Error with Nunjucks paired shortcode `postfixWithZachError`" ) > -1 ); t.true( error.cause.message.startsWith( "Error with Nunjucks paired shortcode `postfixWithZachError`" ) ); t.true( error.cause.originalError.message.startsWith( "Nunjucks sync function paired Shortcode (error throwing)" ) ); }); test("Nunjucks Async function paired Shortcode (error throwing)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode( "postfixWithZachError", async function (str) { throw new Error( "Nunjucks Async function paired Shortcode (error throwing)" ); }, true ); let error = await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZachError name %}hi{% endpostfixWithZachError %}", { name: "test", }); }); t.true( error.message.indexOf( "EleventyNunjucksError: Error with Nunjucks paired shortcode `postfixWithZachError`" ) > -1 ); t.true( error.cause.message.startsWith( "Error with Nunjucks paired shortcode `postfixWithZachError`" ) ); t.true( error.cause.originalError.message.startsWith( "Nunjucks Async function paired Shortcode (error throwing)" ) ); }); test("Nunjucks Shortcode Safe Output", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str) { // Data in context t.is(this.page.url, "/hi/"); return `${str}`; }); t.is( await tr._testRender("{% postfixWithZach name %}", { name: "test", page: { url: "/hi/", }, }), "test" ); }); test("Nunjucks Shortcode return non-string value", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("getYear", function () { return 2022; }); t.is(await tr._testRender("{% getYear %}"), "2022"); }); test("Nunjucks Paired Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return str + content + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name %}Content{% endpostfixWithZach %}", { name: "test", page: { url: "/hi/", }, }), "testContentZach" ); }); test("Nunjucks Async Paired Shortcode", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode( "postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return new Promise(function (resolve) { setTimeout(function () { resolve(str + content + "Zach"); }); }); }, true ); t.is( await tr._testRender("{% postfixWithZach name %}Content{% endpostfixWithZach %}", { name: "test", page: { url: "/hi/", }, }), "testContentZach" ); }); test("Nunjucks Nested Async Paired Shortcode", async (t) => { t.plan(3); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode( "postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return new Promise(function (resolve) { setTimeout(function () { resolve(str + content + "Zach"); }); }); }, true ); t.is( await tr._testRender( "{% postfixWithZach name %}Content{% postfixWithZach name2 %}Content{% endpostfixWithZach %}{% endpostfixWithZach %}", { name: "test", name2: "test2", page: { url: "/hi/", }, } ), "testContenttest2ContentZachZach" ); }); test("Nunjucks Paired Shortcode without args #372", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, arg1) { // Data in context t.is(this.page.url, "/hi/"); return content + arg1 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach %}Content{% endpostfixWithZach %}", { name: "test", page: { url: "/hi/", }, }), "ContentundefinedZach" ); }); test("Nunjucks Paired Shortcode with Tag Inside", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return str + content + "Zach"; }); t.is( await tr._testRender( "{% postfixWithZach name %}Content{% if tester %}If{% endif %}{% endpostfixWithZach %}", { name: "test", tester: true, page: { url: "/hi/", }, } ), "testContentIfZach" ); }); test("Nunjucks Nested Paired Shortcode", async (t) => { t.plan(3); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addPairedShortcode("postfixWithZach", function (content, str) { // Data in context t.is(this.page.url, "/hi/"); return str + content + "Zach"; }); t.is( await tr._testRender( "{% postfixWithZach name %}Content{% postfixWithZach name2 %}Content{% endpostfixWithZach %}{% endpostfixWithZach %}", { name: "test", name2: "test2", page: { url: "/hi/", }, } ), "testContenttest2ContentZachZach" ); }); test("Nunjucks Shortcode Multiple Args", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str, str2) { // Data in context t.is(this.page.url, "/hi/"); return str + str2 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach name, other %}", { name: "test", other: "howdy", page: { url: "/hi/", }, }), "testhowdyZach" ); }); test("Nunjucks Shortcode Multiple Args (Comma is required)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str, str2) { return str + str2 + "Zach"; }); await t.throwsAsync(async () => { await tr._testRender("{% postfixWithZach name other %}", { name: "test", other: "howdy", }); }); }); test("Nunjucks Shortcode Named Args", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (arg) { // Data in context t.is(this.page.url, "/hi/"); return arg.arg1 + arg.arg2 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach arg1=name, arg2=other %}", { name: "test", other: "howdy", page: { url: "/hi/", }, }), "testhowdyZach" ); }); test("Nunjucks Shortcode Named Args (Reverse Order)", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (arg) { // Data in context t.is(this.page.url, "/hi/"); return arg.arg1 + arg.arg2 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach arg2=other, arg1=name %}", { name: "test", other: "howdy", page: { url: "/hi/", }, }), "testhowdyZach" ); }); test("Nunjucks Shortcode Named Args (JS notation)", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (arg) { // Data in context t.is(this.page.url, "/hi/"); return arg.arg1 + arg.arg2 + "Zach"; }); t.is( await tr._testRender("{% postfixWithZach { arg1: name, arg2: other } %}", { name: "test", other: "howdy", page: { url: "/hi/", }, }), "testhowdyZach" ); }); test("Nunjucks Test if statements on arrays (Issue #524)", async (t) => { let tr = await getNewTemplateRender("njk", "./test/stubs/"); t.is( await tr._testRender("{% if 'first' in tags %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); t.is( await tr._testRender("{% if 'sdfsdfs' in tags %}{% else %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); t.is( await tr._testRender("{% if false %}{% elseif 'first' in tags %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); t.is( await tr._testRender("{% if tags.includes('first') %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); t.is( await tr._testRender("{% if tags.includes('dsds') %}{% else %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); t.is( await tr._testRender("{% if false %}{% elseif tags.includes('first') %}Success.{% endif %}", { tags: ["first", "second"], }), "Success." ); }); test("Issue 611: Run a function", async (t) => { // This does not work in Liquid let tr = await getNewTemplateRender("njk", "./test/stubs/"); t.is( await tr._testRender("{{ test() }}", { test: function () { return "alkdsjfksljaZach"; }, }), "alkdsjfksljaZach" ); }); test("Nunjucks bypass compilation", async (t) => { let tr = await getNewTemplateRender("njk"); t.is(tr.engine.needsCompilation("

{{ me }}

"), true); t.is(tr.engine.needsCompilation("

{% tag %}{% endtag %}

"), true); t.is(tr.engine.needsCompilation("

test

"), false); }); test("Nunjucks Parse for Symbols", async (t) => { let tr = await getNewTemplateRender("njk"); let engine = tr.engine; t.deepEqual(engine.parseForSymbols("

{{ name }}

"), ["name"]); t.deepEqual(engine.parseForSymbols("

{{ eleventy.deep.nested }}

"), [ "eleventy.deep.nested", ]); t.deepEqual(engine.parseForSymbols("

{{ a }} {{ b }}

"), ["a", "b"]); t.deepEqual(engine.parseForSymbols("

{% if true %}{{ c }}{% endif %}

"), ["c"]); t.deepEqual(engine.parseForSymbols("

{% if false %}{{ c }}{% endif %}

"), ["c"]); t.deepEqual(engine.parseForSymbols("{{ collections.all[0] }}>"), [ // Note that the Liquid parser returns collections.all[0] "collections.all", ]); t.deepEqual(engine.parseForSymbols("{{ collections.mine }}>"), ["collections.mine"]); t.deepEqual(engine.parseForSymbols("{{ collections.mine | test }}>"), [ // TODO not ideal to have `test` in here? "test", "collections.mine", ]); }); test("Nunjucks Parse for Symbols with custom block", async (t) => { let tr = await getNewTemplateRender("njk"); let engine = tr.engine; engine.config.nunjucksShortcodes.test = function () {}; t.deepEqual(engine.parseForSymbols("

{{ name }} {% test %}

"), ["name"]); }); test("Use addNunjucksGlobal with function", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addNunjucksGlobal("fortytwo", function () { return 42; }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ fortytwo() }}

"); t.is(await fn(), "

42

"); }); test("Use addNunjucksGlobal with literal", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addNunjucksGlobal("fortytwo", 42); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ fortytwo }}

"); t.is(await fn(), "

42

"); }); // Async not supported here test.skip("Use addNunjucksGlobal with async function", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.addNunjucksGlobal("fortytwo", getPromise(42)); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ fortytwo() }}

"); t.is(await fn(), "

42

"); }); test("Use config driven Nunjucks Environment Options (throws on undefined variable)", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.setNunjucksEnvironmentOptions({ throwOnUndefined: true, }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ test }}

"); await t.throwsAsync(async () => { await fn({}); }); }); test("Use config driven Nunjucks Environment Options (autoescape)", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.setNunjucksEnvironmentOptions({ autoescape: false, }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ test }}

"); t.is( await fn({ test: "Hi", }), "

Hi

" ); }); test("Nunjucks Shortcode in a loop (everything is sync)", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-njk-async/" }, function(cfg) { cfg.addNunjucksShortcode("genericshortcode", function (str) { return str; }); } ); let tr = await getNewTemplateRender("njk", "./test/stubs-njk-async/", templateConfig); let fn = await tr.getCompiledTemplate( "{% for item in list %}{% include 'loop.njk' %}{% endfor %}" ); t.is( await fn({ list: ["a", "b", "c"], }), "included_a-aincluded_b-bincluded_c-c" ); }); // TODO! test.skip("Weird issue with number arguments in a loop (not parsing literals properly?)", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-njk-async/" }, function(cfg) { cfg.addNunjucksShortcode("genericshortcode", function (str) { return str; }); } ); let tr = await getNewTemplateRender("njk", "./test/stubs-njk-async/", templateConfig); let fn = await tr.getCompiledTemplate( "{% for item in list %}{{item}}-{% genericshortcode item %}{% endfor %}" ); t.is( await fn({ list: [1, 2, 3], }), "1-12-23-3" ); }); test("Use a precompiled Nunjucks template", async (t) => { // custom loader object let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { cfg.setNunjucksPrecompiledTemplates({ "RenderDirect:BQAPaWxMHTxOqfCSB_bEoWTvtWt-obPbnZTUznRl9LA": (function () { function root(env, context, frame, runtime, cb) { var lineno = 0; var colno = 0; var output = ""; try { var parentTemplate = null; var t_1; t_1 = 34; frame.set("nunjucksVar", t_1, true); if (frame.topLevel) { context.setVariable("nunjucksVar", t_1); } if (frame.topLevel) { context.addExport("nunjucksVar", t_1); } output += runtime.suppressValue( runtime.contextOrFrameLookup(context, frame, "hi"), env.opts.autoescape ); output += "\n"; output += runtime.suppressValue( runtime.contextOrFrameLookup(context, frame, "nunjucksVar"), env.opts.autoescape ); if (parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { cb(null, output); } } catch (e) { cb(runtime.handleError(e, lineno, colno)); } } return { root: root, }; })(), }); } ); await templateConfig.init(); let tr = await getNewTemplateRender("njk", null, templateConfig); // Just pass a unique key in here if you’re using precompiled templates via config. let fn = await tr.getCompiledTemplate("RenderDirect:BQAPaWxMHTxOqfCSB_bEoWTvtWt-obPbnZTUznRl9LA"); t.is( normalizeNewLines( await fn({ hi: "Zach", }) ), `Zach 34` ); }); test("Make sure addFilter is async-friendly for Nunjucks", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // requires async function cfg.addFilter("fortytwo", async function (val, val2) { return getPromise(val + val2); }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ 10 | fortytwo(2) }}

"); t.is(await fn(), "

12

"); }); test("Throw an error when you return a promise in addFilter for Nunjucks", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // requires async function cfg.addFilter("fortytwo", function () { return getPromise(42); }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ 'hi' | fortytwo }}

"); await t.throwsAsync(fn); }); test("addAsyncFilter for Nunjucks", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // works without async function (can return promise) cfg.addAsyncFilter("fortytwo", function (val, val2) { return getPromise(val + val2); }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ 10 | fortytwo(2) }}

"); t.is(await fn(), "

12

"); }); test("Asynchronous filters (via addNunjucksFilter) for Nunjucks", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // works without async function (can return promise) cfg.addNunjucksFilter( "fortytwo", function (value1, value2, callback) { setTimeout(function () { callback(null, value1 + value2); }, 100); }, true ); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ 10 | fortytwo(2) }}

"); t.is(await fn(), "

12

"); }); test("Asynchronous filters (via addNunjucksAsyncFilter) for Nunjucks", async (t) => { let templateConfig = await getTemplateConfigInstanceCustomCallback( {}, function(cfg) { // works without async function (can return promise) cfg.addNunjucksAsyncFilter("fortytwo", function (value1, value2, callback) { setTimeout(function () { callback(null, value1 + value2); }, 100); }); } ); let tr = await getNewTemplateRender("njk", null, templateConfig); let fn = await tr.getCompiledTemplate("

{{ 10 | fortytwo(2) }}

"); t.is(await fn(), "

12

"); }); test("Nunjucks Shortcode with this.env #3175", async (t) => { t.plan(2); let tr = await getNewTemplateRender("njk", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function () { t.truthy(this.env); return "Zach"; }); t.is(await tr._testRender("{% postfixWithZach %}", {}), "Zach"); }); ================================================ FILE: test/TemplateRenderPluginTest.js ================================================ import test from "ava"; import { default as RenderPlugin, File as RenderPluginFile, String as RenderPluginString, RenderManager, } from "../src/Plugins/RenderPlugin.js"; import Eleventy from "../src/Eleventy.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; async function getTestOutput(input, configCallback = function () {}) { let elev = new Eleventy(input, "./_site/", { config: function (eleventyConfig) { eleventyConfig.addPlugin(RenderPlugin); configCallback(eleventyConfig); }, }); elev.setIsVerbose(false); // Careful with this! // elev.disableLogger(); let result = await elev.toJSON(); if (!result.length) { throw new Error(`No Eleventy JSON output found for input: ${input}`); } return result; } async function getTestOutputForFile(inputFile, configCallback) { let result = await getTestOutput(inputFile, configCallback); let html = normalizeNewLines(result[0].content.trim()); return html; } test("Use liquid in nunjucks", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid.njk"); t.is( html, `nunjucksHi 69 * liquidHi test test liquidBye 138` ); }); test("Use liquid+markdown in 11ty.js", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid-md.11ty.cjs"); t.is( html, `

Markdown

  • 2
` ); }); test("Use nunjucks in 11ty.js", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/nunjucks.11ty.cjs"); t.is(html, `* iHtpircsavaj`); }); // This is not yet supported and currently throws an error. test.skip("Use 11ty.js in liquid", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/11tyjs.liquid"); t.is(html, `TESTING`); }); test("Use nunjucks in liquid", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/nunjucks.liquid"); t.is( html, `* iHdiuqil * lfjksdlba` ); }); test("Use markdown in liquid", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/md.liquid"); t.is( html, `

Hello {{ hi }}

  • Testing
` ); }); test("Use nunjucks file in njk (uses renderTemplate inside of the nunjucks file)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/njk-file.njk"); t.is( html, `TESTING TESTING IN LIQUID * 999` ); }); test("Use 11ty.js file in njk", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/11tyjs-file.njk"); t.is(html, `TESTING`); }); // 3.0 breaking change, we can’t alias to 11ty.js any more test.skip("Breaking Change (3.0): Use txt file in njk (override to 11ty.js)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/11tyjs-file-override.njk"); t.is(html, `TESTING`); }); // Skip this for now, toJSON calls actually change the exitCode of the process when they error, // which is not ideal. test.skip("Use nunjucks file in liquid but it doesn’t exist", async (t) => { await t.throwsAsync(async () => { await getTestOutputForFile("./test/stubs-render-plugin/njk-file-not-exist.liquid"); }); }); test("No syntax passed, uses parent page syntax: liquid", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/false.liquid"); t.is( html, `# Hello Bruno * Testing` ); }); test("No syntax passed (uses parent page syntax), but does pass data: liquid", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/data-no-templatelang.liquid"); t.is( html, `# Hello Bruno * Testing` ); }); // Not yet supported test.skip("renderFile but the target has front matter.", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/using-frontmatter.liquid"); t.is(html, `frontmatterString`); }); // Idea from https://twitter.com/raymondcamden/status/1460961620247650312 test("Capture nunjucks render output to a liquid variable", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/capture-njk.liquid"); t.is(html, `4`); }); // Idea from https://twitter.com/raymondcamden/status/1460961620247650312 // Possibly blocked by async in {% set %} https://github.com/mozilla/nunjucks/issues/815 test("Capture liquid render output to a njk variable", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/capture-liquid.njk"); t.is(html, `4`); }); test("Remap non-object data to data._ if object is not passed in", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/bad-data.njk"); t.is(html, "string"); }); test("Direct use of render string plugin, rendering Nunjucks (and nested Liquid)", async (t) => { let renderMgr = new RenderManager(); renderMgr.config(function (eleventyConfig) { eleventyConfig.addFilter("testing", function () { return "tested."; }); }); let fn = await renderMgr.compile( `{%- set nunjucksVar = 69 -%} {{ hi }} {{ nunjucksVar }} {{ "who" | testing }} {% renderTemplate "liquid", argData %} {% assign liquidVar = 138 %} * {{ hi }} test test {{ bye }} {{ liquidVar }} {% endrenderTemplate %} `, "njk" ); let data = { hi: "nunjucksHi", argData: { hi: "liquidHi", bye: "liquidBye", }, }; let html = await fn(data); t.is( normalizeNewLines(html.trim()), `nunjucksHi 69 tested. * liquidHi test test liquidBye 138` ); }); test("Direct use of render string plugin, rendering Liquid (and nested Nunjucks)", async (t) => { let renderMgr = new RenderManager(); renderMgr.config(function (eleventyConfig) { eleventyConfig.addFilter("testing", function () { return "tested."; }); }); let fn = await renderMgr.compile( `{%- assign liquidVar = 69 -%} {{ hi }} {{ liquidVar }} {{ "who" | testing }} {% renderTemplate "njk", argData %} {% set njkVar = 138 %} * {{ hi }} test test {{ bye }} {{ njkVar }} {% endrenderTemplate %} `, "liquid" ); let data = { hi: "liquidHi", argData: { hi: "njkHi", bye: "njkBye", }, }; let html = await fn(data); t.is( normalizeNewLines(html.trim()), `liquidHi 69 tested. * njkHi test test njkBye 138` ); }); test("Direct use of render file plugin, rendering Nunjucks (and nested Liquid)", async (t) => { let fn = await RenderPluginFile("./test/stubs-render-plugin/liquid-direct.njk", { config: function (eleventyConfig) { eleventyConfig.addPlugin(RenderPlugin); eleventyConfig.addFilter("testing", function () { return "tested."; }); }, }); let data = { hi: "liquidHi", argData: { hi: "njkHi", bye: "njkBye", }, }; let html = await fn(data); t.is( normalizeNewLines(html.trim()), `liquidHi 69 tested. * njkHi test test njkBye 138` ); }); test("Use page in renderTemplate (liquid in liquid)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid-page.liquid"); t.is(html, `/liquid-page/`); }); test("Use page in renderTemplate (liquid in njk)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid-page.njk"); t.is(html, `/liquid-page/`); }); test("Use page in renderTemplate (njk in liquid)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/njk-page.liquid"); t.is(html, `/njk-page/`); }); test("Use eleventy in renderTemplate (njk in liquid)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/njk-eleventy.liquid"); t.true(html.startsWith("4.")); }); test("Use eleventy in renderTemplate (liquid in njk)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid-eleventy.njk"); t.true(html.startsWith("4.")); }); test.skip("Use nunjucks in liquid (access to all global data)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/nunjucks-global.liquid"); t.is(html, `globalHi`); }); test.skip("Use liquid in njk (access to all global data)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/liquid-global.njk"); t.is(html, `globalHi`); }); test("renderContent filter #3369 #3370 via renderTemplate (njk)", async (t) => { let html = await getTestOutputForFile("./test/stubs-render-plugin/nunjucks-frontmatter.njk", (eleventyConfig) => { eleventyConfig.addShortcode("test", () => "test content") }); t.is(html, "test content"); }); test("#3368 #3810 config init bug with RenderManager", async (t) => { let elev = new Eleventy("./test/stubs-3810/", false, { configPath: "./test/stubs-3810/eleventy.config.js", }); let results = await elev.toJSON(); t.is(results[0].content, `

Sign up for our newsletter!

`); }); ================================================ FILE: test/TemplateRenderTest.js ================================================ import test from "ava"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function getNewTemplateRender(name, inputDir) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } }); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); tr.extensionMap.engineManager = new TemplateEngineManager(eleventyConfig); tr.extensionMap.setFormats([]); await tr.init(); return tr; } test("Basic", async (t) => { await t.throwsAsync(async () => { let tr = await getNewTemplateRender("sldkjfkldsj"); tr.init("sldkjfkldsj"); }); }); test("Includes Dir", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs"); t.is(tr.getIncludesDir(), "./test/stubs/_includes/"); }); test("Invalid override", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs"); await t.throwsAsync(async () => { await tr.setEngineOverride("lslkdjf"); }); }); test("Valid Override", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs"); await tr.setEngineOverride("njk"); t.is(tr.getEngineName(), "njk"); t.truthy(tr.isEngine("njk")); }); test("Parse Overrides to get Prioritized Engine List", async (t) => { t.deepEqual(TemplateRender.parseEngineOverrides(""), []); t.deepEqual(TemplateRender.parseEngineOverrides(null), []); t.deepEqual(TemplateRender.parseEngineOverrides(undefined), []); t.deepEqual(TemplateRender.parseEngineOverrides(false), []); t.deepEqual(TemplateRender.parseEngineOverrides("html"), []); t.deepEqual(TemplateRender.parseEngineOverrides("html,html"), []); t.deepEqual(TemplateRender.parseEngineOverrides("html,md,md"), ["md"]); t.deepEqual(TemplateRender.parseEngineOverrides("liquid,md"), ["md", "liquid"]); t.deepEqual(TemplateRender.parseEngineOverrides("liquid"), ["liquid"]); t.deepEqual(TemplateRender.parseEngineOverrides("njk"), ["njk"]); t.deepEqual(TemplateRender.parseEngineOverrides("liquid,html"), ["liquid"]); t.deepEqual(TemplateRender.parseEngineOverrides("liquid,md,html"), ["md", "liquid"]); t.deepEqual(TemplateRender.parseEngineOverrides("njk,njk"), ["njk"]); t.throws(function () { TemplateRender.parseEngineOverrides("njk,liquid"); }); t.throws(function () { TemplateRender.parseEngineOverrides("liquid,njk,html"); }); }); test("Make sure getEnginesList returns a string", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs"); t.is(tr.getEnginesList("njk,md"), "njk,md"); }); ================================================ FILE: test/TemplateTest-CompileOptions.js ================================================ import test from "ava"; import TemplateData from "../src/Data/TemplateData.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; test("Custom extension (.txt) with custom permalink compile function", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, // pass in your own custom permalink function. permalink: async function (permalinkString, inputPath) { t.is(permalinkString, "custom-extension.lit"); t.is(inputPath, "./test/stubs/custom-extension.txt"); return async function () { return "HAHA_THIS_ALWAYS_GOES_HERE.txt"; }; }, }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, "/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.path, "./dist/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.rawPath, "HAHA_THIS_ALWAYS_GOES_HERE.txt"); }); test("Custom extension with and compileOptions.permalink = false", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, permalink: false, }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, false); t.is(testObj.path, false); t.is(testObj.rawPath, false); }); test("Custom extension with and opt-out of permalink compilation", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, permalink: "raw", }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, "/custom-extension.lit"); t.is(testObj.path, "./dist/custom-extension.lit"); t.is(testObj.rawPath, "custom-extension.lit"); }); test("Custom extension (.txt) with custom permalink compile function but no permalink in the data cascade", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, // pass in your own custom permalink function. permalink: async function (permalinkString, inputPath) { t.is(permalinkString, undefined); t.is(inputPath, "./test/stubs/custom-extension-no-permalink.txt"); return async function () { return "HAHA_THIS_ALWAYS_GOES_HERE.txt"; }; }, }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension-no-permalink.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, "/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.path, "./dist/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.rawPath, "HAHA_THIS_ALWAYS_GOES_HERE.txt"); }); test("Custom extension (.txt) with custom permalink compile function (that returns a string not a function) but no permalink in the data cascade", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, permalink: async function (permalinkString, inputPath) { t.is(permalinkString, undefined); t.is(inputPath, "./test/stubs/custom-extension-no-permalink.txt"); // unique part of this test: this is a string, not a function return "HAHA_THIS_ALWAYS_GOES_HERE.txt"; }, }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension-no-permalink.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, "/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.path, "./dist/HAHA_THIS_ALWAYS_GOES_HERE.txt"); t.is(testObj.rawPath, "HAHA_THIS_ALWAYS_GOES_HERE.txt"); }); test("Custom extension (.txt) with custom permalink compile function that returns false", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, permalink: async function (permalinkString, inputPath) { t.is(permalinkString, undefined); t.is(inputPath, "./test/stubs/custom-extension-no-permalink.txt"); // unique part of this test: this returns false return false; }, }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension-no-permalink.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); let testObj = await tmpl.getOutputLocations(data); t.is(testObj.href, false); t.is(testObj.path, false); t.is(testObj.rawPath, false); }); test("Custom extension (.txt) that returns undefined from compile", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, compile: function (str, inputPath) { t.is(str, "Sample content"); return function (data) { return undefined; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension-no-permalink.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), undefined); let pages = await tmpl.getTemplates(data); for (let page of pages) { page.templateContent = undefined; t.is(page.templateContent, undefined); // shouldn’t throw an error } }); ================================================ FILE: test/TemplateTest-ComputedData.js ================================================ import test from "ava"; import Eleventy from "../src/Eleventy.js"; import TemplateData from "../src/Data/TemplateData.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback, sortEleventyResults } from "./_testHelpers.js"; async function getRenderedData(tmpl, pageNumber = 0) { let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); return templates[pageNumber].data; } test("eleventyComputed", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/first.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is((await renderTemplate(tmpl, data)).trim(), "hi:value2-value1.css"); }); test("eleventyComputed overrides existing value.", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/override.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is(data.key1, "override"); t.is((await renderTemplate(tmpl, data)).trim(), "hi:override"); }); test("eleventyComputed overrides existing value and reuses that upstream value", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/override-reuse.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is(data.key1, "over(value1)ride"); t.is((await renderTemplate(tmpl, data)).trim(), "hi:over(value1)ride"); }); test("eleventyComputed permalink", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/permalink.njk", "./test/stubs/", "./dist" ); let templates = await tmpl.getTemplates(await tmpl.getData()); let data = templates[0].data; t.is(data.page.url, "/haha-value1.html"); t.is(data.page.outputPath, "./dist/haha-value1.html"); t.is(data.permalink, "haha-value1.html"); t.is(data.nested.key3, "value1"); t.is(data.nested.key4, "depends on computed value1"); t.is(data.dependsOnPage, "depends:/haha-value1.html"); }); test("eleventyComputed simple permalink", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/permalink-simple.njk", "./test/stubs/", "./dist" ); let templates = await tmpl.getTemplates(await tmpl.getData()); let data = templates[0].data; t.is(data.page.url, "/haha-value1.html"); t.is(data.page.outputPath, "./dist/haha-value1.html"); t.is(data.permalink, "haha-value1.html"); }); test("eleventyComputed permalink using slug", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/permalink-slug.njk", "./test/stubs/", "./dist" ); let templates = await tmpl.getTemplates(await tmpl.getData()); let data = templates[0].data; t.is(data.page.url, "/haha-this-is-a-string.html"); t.is(data.page.outputPath, "./dist/haha-this-is-a-string.html"); t.is(data.permalink, "haha-this-is-a-string.html"); }); test("eleventyComputed js front matter (function)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/second.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is(data.key3, "value3-value2-value1.css"); t.is((await renderTemplate(tmpl, data)).trim(), "hi:value2-value1.css"); }); test("eleventyComputed js front matter key reuses and overrides", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/third.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is(data.key1, "value2-value1"); t.is((await renderTemplate(tmpl, data)).trim(), "hi:value2-value1"); }); test("eleventyComputed true primitive", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/true.njk", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); t.is(data.key1, "value1"); t.is(data.key2, true); t.is(data.key3, false); t.is(data.key4, 324); }); test("eleventyComputed relies on global data", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist", } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/eleventyComputed/use-global-data.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let fetchedData = await tmpl.getData(); let templates = await tmpl.getTemplates(fetchedData); let data = templates[0].data; t.is(data.image, "datavalue1"); }); test("eleventyComputed intermixes with global data", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-computed-global", output: "dist", }, function(cfg) { // Defaulted in v1 // cfg.setDataDeepMerge(true); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs-computed-global/intermix.njk", "./test/stubs-computed-global/", "./dist", dataObj, null, eleventyConfig ); let fetchedData = await tmpl.getData(); t.truthy(fetchedData.eleventyComputed.image); t.truthy(fetchedData.eleventyComputed.image2); t.truthy(fetchedData.eleventyComputed.image3); t.truthy(fetchedData.eleventyComputed.eleventyNavigation.key); let templates = await tmpl.getTemplates(fetchedData); let data = templates[0].data; t.is(data.image, "first"); t.is(data.image2, "second"); t.is(data.image3, "third-global"); t.is(data.eleventyNavigation.key, "nested-first-global"); }); test("eleventyComputed using symbol parsing on template strings (nunjucks)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-computed-symbolparse", output: "dist", }, function(cfg) { cfg.addNunjucksFilter("fail", function (str) { // Filter expects a certain String format, don’t use the (((11ty))) string hack if (!str || str.length !== 1) { throw new Error("Expect a one character string"); } return `${str}`; }); }); let tmpl = await getNewTemplate( "./test/stubs-computed-symbolparse/test.njk", "./test/stubs-computed-symbolparse/", "./test/stubs-computed-symbolparse/_site", null, null, eleventyConfig ); let data = await getRenderedData(tmpl); t.is(data.a, "a"); t.is(data.b, "b"); t.is(data.c, "ab"); }); test("eleventyComputed using symbol parsing on template strings (liquid)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-computed-symbolparse", output: "dist", }, function(cfg) { cfg.addLiquidFilter("fail", function (str) { // Filter expects a certain String format, don’t use the (((11ty))) string hack if (!str || str.length !== 1) { throw new Error("Expect a one character string: " + str); } return `${str}`; }); }); let tmpl = await getNewTemplate( "./test/stubs-computed-symbolparse/test.liquid", "./test/stubs-computed-symbolparse/", "./test/stubs-computed-symbolparse/_site", null, null, eleventyConfig ); let data = await getRenderedData(tmpl); t.is(data.a, "a"); t.is(data.b, "b"); t.is(data.c, "ab"); }); test("eleventyComputed render strings in arrays", async (t) => { let tmpl = await getNewTemplate( "./test/stubs-computed-array/test.liquid", "./test/stubs-computed-array/", "./test/stubs-computed-array/_site" ); let data = await getRenderedData(tmpl); t.deepEqual(data.array, ["static value", "test"]); t.is(data.notArray, "test"); }); test("Issue #3728 Computed data with arrays of different sizes, arrays are treated as a single unit", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("index.njk", `---js const arr = [1,2,3,4,5]; const eleventyComputed = { arr: ["a", "b", "c"] } --- {{ arr }}`); } }); elev.disableLogger(); let result = await elev.toJSON(); t.is(result.length, 1); t.is(result[0].content, `a,b,c`); }); test("Issue #3827 with Computed data with arrays and layouts (×2 eleventyComputed)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.setUseTemplateCache(false); eleventyConfig.addTemplate("_includes/base.njk", `---js const eleventyComputed = { arr: ["base", "base2"] }; --- {{ content | safe }}`); eleventyConfig.addTemplate("page1.njk", `---js const layout = "base.njk"; --- {{ arr }}`); eleventyConfig.addTemplate("page2.njk", `---js const layout = "base.njk"; const eleventyComputed = { arr: ["override", "override2"] } --- {{ arr }}`); } }); elev.disableLogger(); let result = await elev.toJSON(); result.sort(sortEleventyResults); t.is(result.length, 2); t.is(result[0].inputPath, `./test/stubs-virtual/page2.njk`); // A merge happened here because it was eleventyComputed -> eleventyComputed array merge before computed data t.is(result[0].content, `base,base2,override,override2`); t.is(result[1].inputPath, `./test/stubs-virtual/page1.njk`); t.is(result[1].content, `base,base2`); }); test("Issue #3728 with Computed data with arrays and layouts (×1 eleventyComputed)", async (t) => { let elev = new Eleventy("./test/stubs-virtual/", undefined, { config: eleventyConfig => { eleventyConfig.addTemplate("_includes/base.njk", `---js const arr = ["base", 1]; --- {{ content | safe }}`); eleventyConfig.addTemplate("page1.njk", `---js const layout = "base.njk"; --- {{ arr }}`); eleventyConfig.addTemplate("page2.njk", `---js const layout = "base.njk"; const eleventyComputed = { arr: ["override", 2] } --- {{ arr }}`); } }); elev.disableLogger(); let result = await elev.toJSON(); result.sort(sortEleventyResults); t.is(result.length, 2); t.is(result[0].content, `override,2`); t.is(result[1].content, `base,1`); }); ================================================ FILE: test/TemplateTest-CustomExtensions.js ================================================ import test from "ava"; import { marked } from "marked"; import TemplateData from "../src/Data/TemplateData.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; test("Using getData: false without getInstanceFromInputPath works ok", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: false, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }) let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "Sample content"); }); test("Using getData: true without getInstanceFromInputPath should error", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: true, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }) let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); await t.throwsAsync(async () => { await tmpl.getData(); }, { message: "`getInstanceFromInputPath` callback missing from \'txt\' template engine plugin. It is required when `getData` is in use. You can set `getData: false` to opt-out of this." }); }); test("Using getData: [] without getInstanceFromInputPath should error", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: [], compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); await t.throwsAsync(async () => { await tmpl.getData(); }, { message: "`getInstanceFromInputPath` callback missing from \'txt\' template engine plugin. It is required when `getData` is in use. You can set `getData: false` to opt-out of this." }); }); test("Using getData: true and getInstanceFromInputPath to get data from instance", async (t) => { let globalData = { topLevelData: true, }; let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: true, getInstanceFromInputPath: function () { return { data: globalData, }; }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.topLevelData, true); }); test("Using eleventyDataKey to get a different key data from instance", async (t) => { let globalData = { topLevelData: true, }; let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: [], getInstanceFromInputPath: function () { return { eleventyDataKey: ["otherProp"], otherProp: globalData, }; }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.topLevelData, true); }); test("Uses default renderer (no compile function) when you override an existing extension", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "liquid", key: "liquid", compileOptions: { cache: false, }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "hi"); }); test("Access to default renderer when you override an existing extension", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "liquid", key: "liquid", compileOptions: { cache: false, }, compile: function (str, inputPath) { // plaintext return function (data) { t.true(true); return this.defaultRenderer(); }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "hi"); }); test("Overridden liquid gets used from a markdown template", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "liquid", key: "liquid", compileOptions: { cache: false, }, compile: function (str, inputPath) { t.true(true); // plaintext return function (data) { return this.defaultRenderer(); }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.md", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is((await renderTemplate(tmpl, data)).trim(), "

hi

"); }); test("Use marked for markdown", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "md", key: "md", compileOptions: { cache: false, }, compile: function (str, inputPath) { let html = marked.parse(str); // plaintext return function (data) { t.true(true); return html; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default-no-liquid.md", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is((await renderTemplate(tmpl, data)).trim(), "

hi

"); }); test("Use defaultRenderer for markdown", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "md", key: "md", compileOptions: { cache: false, }, compile: function (str, inputPath) { return function (data) { t.true(true); return this.defaultRenderer(data); }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.md", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is((await renderTemplate(tmpl, data)).trim(), "

hi

"); }); test("Front matter in a custom extension", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, compile: function (str, inputPath) { return function (data) { return str; }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default-frontmatter.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.frontmatter, 1); t.is((await renderTemplate(tmpl, data)).trim(), "hi"); }); test("Access to default renderer when you override an existing extension (async compile function, arrow render function)", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "liquid", key: "liquid", compileOptions: { cache: false, }, compile: async function (str, inputPath) { // plaintext return async (data) => { t.true(true); return this.defaultRenderer(); }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "hi"); }); test("Access to default renderer when you override an existing extension (async compile function, async render function)", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "liquid", key: "liquid", compileOptions: { cache: false, }, compile: async function (str, inputPath) { // plaintext return async function (data) { t.true(true); return this.defaultRenderer(); }; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/default.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), "hi"); }); test("Return undefined in compile to ignore #2267", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: false, compile: function (str, inputPath) { return; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), undefined); }); test("Return undefined in compile to ignore (async compile function) #2350", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs" }, function(cfg) { cfg.extensionMap.add({ extension: "txt", key: "txt", compileOptions: { cache: false, }, getData: false, compile: async function (str, inputPath) { return; }, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/custom-extension.txt", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await renderTemplate(tmpl, data), undefined); }); ================================================ FILE: test/TemplateTest-DataCascade.js ================================================ import test from "ava"; import TemplateData from "../src/Data/TemplateData.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; // Prior to and including 0.10.0 this mismatched the documentation)! (Issue #915) test("Layout front matter does not override template data files", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-data-cascade/layout-data-files", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-data-cascade/layout-data-files/test.njk", "./test/stubs-data-cascade/layout-data-files/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.shared, "datafile"); }); test("Layout front matter should not override global data (sanity check, Issue 915)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-data-cascade/global-versus-layout", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-data-cascade/global-versus-layout/test.njk", "./test/stubs-data-cascade/global-versus-layout/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.cascade, "from-layout-file"); }); test("Template data files should be more specific in data cascade than Layout front matter (breaking change in 1.0, issue 915)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-data-cascade/layout-versus-tmpldatafile", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-data-cascade/layout-versus-tmpldatafile/test.njk", "./test/stubs-data-cascade/layout-versus-tmpldatafile/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.cascade, "template-data-file"); }); test("Directory data files should be more specific in data cascade than Layout front matter (breaking change in 1.0, issue 915)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs-data-cascade/layout-versus-dirdatafile/src/", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); let tmpl = await getNewTemplate( "./test/stubs-data-cascade/layout-versus-dirdatafile/src/test.njk", "./test/stubs-data-cascade/layout-versus-dirdatafile/src/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.cascade, "dir-data-file"); }); ================================================ FILE: test/TemplateTest-Dates.js ================================================ import test from "ava"; import getNewTemplate from "./_getNewTemplateForTests.js"; async function getRenderedData(tmpl, pageNumber = 0) { let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); return templates[pageNumber].data; } test("getMappedDate (empty, assume created)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/dates/file1.md", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); }); test("getMappedDate (explicit date, yaml String)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/dates/file2.md", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); t.is(Date.UTC(2016, 0, 1), date.getTime()); }); test("getMappedDate (explicit date, yaml Date)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/dates/file2b.md", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); t.is(Date.UTC(2016, 0, 1), date.getTime()); }); test("getMappedDate (explicit date, yaml Date and string should be the same)", async (t) => { let tmplA = await getNewTemplate("./test/stubs/dates/file2.md", "./test/stubs/", "./dist"); let dataA = await getRenderedData(tmplA); let stringDate = await tmplA.getMappedDate(dataA); let tmplB = await getNewTemplate("./test/stubs/dates/file2b.md", "./test/stubs/", "./dist"); let dataB = await getRenderedData(tmplB); let yamlDate = await tmplB.getMappedDate(dataB); t.truthy(stringDate); t.truthy(yamlDate); t.deepEqual(stringDate, yamlDate); }); test("getMappedDate (modified date)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/dates/file3.md", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); }); test("getMappedDate (created date)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/dates/file4.md", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); }); test("getMappedDate (falls back to filename date)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/dates/2018-01-01-file5.md", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); t.is(Date.UTC(2018, 0, 1), date.getTime()); }); test("getMappedDate (found multiple dates, picks first)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/dates/2019-01-01-folder/2020-01-01-file.md", "./test/stubs/", "./dist" ); let data = await getRenderedData(tmpl); let date = await tmpl.getMappedDate(data); t.true(date instanceof Date); t.truthy(date.getTime()); t.is(Date.UTC(2019, 0, 1), date.getTime()); }); ================================================ FILE: test/TemplateTest-JavaScript.js ================================================ import test from "ava"; import semver from "semver"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getRenderedTemplates as getRenderedTmpls } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; test("JavaScript template type (function)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/function.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/function/index.html"); data.name = "Zach"; let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Zach

"); // New in 2.0.0-canary.19 Issue #1522 t.is(pages[0].content.trim(), "

Zach

"); t.is(pages[0].page.url, "/function/"); }); test("JavaScript template type (class with data getter)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/class-data.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-data/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Ted

"); t.is(pages[0].content.trim(), "

Ted

"); }); test("JavaScript template type (class with data method)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/class-data-fn.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-data-fn/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Ted

"); t.is(pages[0].content.trim(), "

Ted

"); }); if (semver.gte(process.version, "12.4.0")) { test("JavaScript template type (class fields)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/classfields-data.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/classfields-data/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Ted

"); t.is(pages[0].content.trim(), "

Ted

"); }); } test("JavaScript template type (class with shorthand data method)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-fn-shorthand.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-data-fn-shorthand/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Ted

"); t.is(pages[0].content.trim(), "

Ted

"); }); test("JavaScript template type (class with async data method)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-async-data-fn.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-async-data-fn/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "

Ted

"); t.is(pages[0].content.trim(), "

Ted

"); }); test("JavaScript template type (class with data getter and a javascriptFunction)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.addJavaScriptFunction("upper", function (val) { return new String(val).toUpperCase(); }); }); let tmpl = await getNewTemplate( "./test/stubs/class-data-filter.11ty.cjs", "./test/stubs/", "./dist", null, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-data-filter/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].content.trim(), "

TED

"); t.is(pages[0].templateContent.trim(), "

TED

"); }); test("JavaScript template type (class with data method and a javascriptFunction)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist", }, function(cfg) { cfg.addJavaScriptFunction("upper", function (val) { return new String(val).toUpperCase(); }); }); let tmpl = await getNewTemplate( "./test/stubs/class-data-fn-filter.11ty.cjs", "./test/stubs/", "./dist", null, null, eleventyConfig ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/class-data-fn-filter/index.html"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].content.trim(), "

TED

"); t.is(pages[0].templateContent.trim(), "

TED

"); }); test("JavaScript template type (class with data permalink)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/index.html"); }); test("JavaScript template type (class with data permalink using a buffer)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink-buffer.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/index.html"); }); test("JavaScript template type (class with data permalink function)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink-fn.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/value1/index.html"); }); test("JavaScript template type (class with data permalink function using a buffer)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink-fn-buffer.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/value1/index.html"); }); test("JavaScript template type (class with data permalink async function)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink-async-fn.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/value1/index.html"); }); test("JavaScript template type (class with data permalink function using a filter)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-data-permalink-fn-filter.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/my-permalink/my-super-cool-title/index.html"); }); test("JavaScript template type (should use the same class instance for data and render)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/oneinstance.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let pages = await getRenderedTmpls(tmpl, data); // the template renders the random number created in the class constructor // the data returns the random number created in the class constructor // if they are different, the class is not reused. t.is(pages[0].content.trim(), `

Ted${data.rand}

`); t.is(pages[0].templateContent.trim(), `

Ted${data.rand}

`); }); test("JavaScript template type (multiple exports)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/multipleexports.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].content.trim(), "

Ted

"); t.is(pages[0].templateContent.trim(), "

Ted

"); }); test("JavaScript template type (multiple exports, promises)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/multipleexports-promises.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(data.name, "Ted"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].content.trim(), "

Ted

"); t.is(pages[0].templateContent.trim(), "

Ted

"); }); test("JavaScript template type (object)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/object.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(data.name, "Ted"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].content.trim(), "

Ted

"); t.is(pages[0].templateContent.trim(), "

Ted

"); }); test("JavaScript template type (object, no render method)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/object-norender.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(data.name, "Ted"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), ""); t.is(pages[0].content.trim(), ""); }); test("JavaScript template type (class, no render method)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/class-norender.11ty.cjs", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(data.name, "Ted"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), ""); t.is(pages[0].content.trim(), ""); }); test("JavaScript template type (data returns a string)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/exports-flatdata.11ty.cjs", "./test/stubs/", "./dist" ); await t.throwsAsync(async () => { await tmpl.getData(); }); }); ================================================ FILE: test/TemplateTest.js ================================================ import test from "ava"; import fs from "fs"; import pretty from "pretty"; import TOML from "@iarna/toml"; import TemplateData from "../src/Data/TemplateData.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import EleventyErrorUtil from "../src/Errors/EleventyErrorUtil.js"; import TemplateContentPrematureUseError from "../src/Errors/TemplateContentPrematureUseError.js"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getRenderedTemplates as getRenderedTmpls, renderLayout, renderTemplate } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; async function getRenderedData(tmpl, pageNumber = 0) { let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); return templates[pageNumber].data; } function cleanHtml(str) { return pretty(str, { ocd: true }); } async function _testCompleteRender(tmpl) { let data = await tmpl.getData(); let entries = await tmpl.getTemplateMapEntries(data); let nestedContent = await Promise.all( entries.map(async (entry) => { entry._pages = await entry.template.getTemplates(entry.data); return Promise.all( entry._pages.map(async (page) => { page.templateContent = await page.template.renderPageEntryWithoutLayout(page); return page.template.renderPageEntry(page); }) ); }) ); let contents = [].concat(...nestedContent); return contents; } test("getTemplateSubFolder", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); t.is(tmpl.getTemplateSubfolder(), ""); }); test("getTemplateSubFolder, output is a subdir of input", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/template.liquid", "./test/stubs/", "./test/stubs/_site" ); t.is(tmpl.getTemplateSubfolder(), ""); }); test("output path maps to an html file", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); t.is(tmpl.inputDir, "./test/stubs/"); t.is(tmpl.outputDir, "./dist/"); t.is(tmpl.getTemplateSubfolder(), ""); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/template/index.html"); }); test("subfolder outputs to a subfolder", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/subfolder/subfolder.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(tmpl.parsed.dir, "./test/stubs/subfolder"); t.is(tmpl.getTemplateSubfolder(), "subfolder"); t.is(await tmpl.getOutputPath(data), "./dist/subfolder/index.html"); }); test("subfolder outputs to double subfolder", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/subfolder/subfolder/subfolder.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(tmpl.parsed.dir, "./test/stubs/subfolder/subfolder"); t.is(tmpl.getTemplateSubfolder(), "subfolder/subfolder"); t.is(await tmpl.getOutputPath(data), "./dist/subfolder/subfolder/index.html"); }); test("HTML files output to the same as the input directory have a file suffix added (only if index, this is not index).", async (t) => { let tmpl = await getNewTemplate("./test/stubs/testing.html", "./test/stubs", "./test/stubs"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./test/stubs/testing/index.html"); }); test("HTML files output to the same as the input directory have a file suffix added (only if index, this _is_ index).", async (t) => { let tmpl = await getNewTemplate("./test/stubs/index.html", "./test/stubs", "./test/stubs"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./test/stubs/index.html"); }); test("HTML files output to the same as the input directory have a file suffix added (only if index, this _is_ index, subfolder).", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/subfolder/index.html", "./test/stubs", "./test/stubs" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./test/stubs/subfolder/index.html"); }); test("Test raw front matter from template (yaml)", async (t) => { // https://github.com/jonschlinkert/gray-matter/blob/master/examples/yaml.js let tmpl = await getNewTemplate( "./test/stubs/templateFrontMatter.liquid", "./test/stubs/", "./dist" ); t.truthy(await tmpl.getInputContent(), "template exists and can be opened."); t.is((await tmpl._testGetFrontMatter()).data.key1, "value1"); t.is((await tmpl._testGetFrontMatter()).data.key3, "value3"); let data = await tmpl.getData(); t.is(data.key1, "value1"); t.is(data.key3, "value3"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "c:value1:value2:value3"); }); test("Test raw front matter from template (json)", async (t) => { // https://github.com/jonschlinkert/gray-matter/blob/master/examples/json.js let tmpl = await getNewTemplate( "./test/stubs/templateFrontMatterJson.liquid", "./test/stubs/", "./dist" ); t.is((await tmpl._testGetFrontMatter()).data.key1, "value1"); t.is((await tmpl._testGetFrontMatter()).data.key3, "value3"); let data = await tmpl.getData(); t.is(data.key1, "value1"); t.is(data.key3, "value3"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "c:value1:value2:value3"); }); test("Test raw front matter from template (js)", async (t) => { // https://github.com/jonschlinkert/gray-matter/blob/master/examples/javascript.js let tmpl = await getNewTemplate( "./test/stubs/templateFrontMatterJs.njk", "./test/stubs/", "./dist" ); t.is((await tmpl._testGetFrontMatter()).data.key1, "value1"); t.is((await tmpl._testGetFrontMatter()).data.key3, "value3"); let data = await tmpl.getData(); t.is(data.key1, "value1"); t.is(data.key3, "value3"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "c:value1:VALUE2:value3"); }); test("Test that getData() works", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/templateFrontMatter.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(data.key1, "value1"); t.is(data.key3, "value3"); }); test("One Layout (using new content var)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/templateWithLayoutKey.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is((await tmpl._testGetFrontMatter()).data[tmpl.config.keys.layout], "defaultLayout"); let data = await tmpl.getData(); t.is(data[tmpl.config.keys.layout], "defaultLayout"); t.is( normalizeNewLines(cleanHtml(await renderLayout(tmpl, data))), `

Hello.

` ); t.is(data.keymain, "valuemain"); t.is(data.keylayout, "valuelayout"); }); test("One Layout (using content)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/templateWithLayoutContent.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is((await tmpl._testGetFrontMatter()).data[tmpl.config.keys.layout], "defaultLayoutLayoutContent"); let data = await tmpl.getData(); t.is(data[tmpl.config.keys.layout], "defaultLayoutLayoutContent"); t.is( normalizeNewLines(cleanHtml(await renderLayout(tmpl, data))), `

Hello.

` ); t.is(data.keymain, "valuemain"); t.is(data.keylayout, "valuelayout"); }); test("One Layout (layouts disabled)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/templateWithLayoutContent.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is((await tmpl._testGetFrontMatter()).data[tmpl.config.keys.layout], "defaultLayoutLayoutContent"); let data = await tmpl.getData(); t.is(data[tmpl.config.keys.layout], "defaultLayoutLayoutContent"); t.is(cleanHtml(await tmpl.renderPageEntryWithoutLayout({ rawInput: await tmpl.getPreRender(), data: data })), "

Hello.

"); t.is(data.keymain, "valuemain"); t.is(data.keylayout, "valuelayout"); }); test("One Layout (liquid test)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/templateWithLayout.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is((await tmpl._testGetFrontMatter()).data[tmpl.config.keys.layout], "layoutLiquid.liquid"); let data = await tmpl.getData(); t.is(data[tmpl.config.keys.layout], "layoutLiquid.liquid"); t.is( normalizeNewLines(cleanHtml(await renderLayout(tmpl, data))), `

Hello.

` ); t.is(data.keymain, "valuemain"); t.is(data.keylayout, "valuelayout"); }); test("Two Layouts", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/templateTwoLayouts.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is((await tmpl._testGetFrontMatter()).data[tmpl.config.keys.layout], "layout-a"); let data = await tmpl.getData(); t.is(data[tmpl.config.keys.layout], "layout-a"); t.is(data.key1, "value1"); t.is( normalizeNewLines(cleanHtml(await renderLayout(tmpl, data))), `

value2-a

` ); t.is(data.daysPosted, 152); }); test("Liquid template", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/formatTest.liquid", "./test/stubs/", "dist", dataObj, null, eleventyConfig ); t.is(await renderTemplate(tmpl, await tmpl.getData()), "

Zach

"); }); test("Liquid template with include", async (t) => { let tmpl = await getNewTemplate("./test/stubs/includer.liquid", "./test/stubs/", "dist"); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

This is an include.

"); }); test("Permalink output directory", async (t) => { let tmpl = await getNewTemplate("./test/stubs/permalinked.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/permalinksubfolder/index.html"); }); test("Permalink output directory from layout", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-in-layout.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/hello/index.html"); }); test("Permalink output directory from layout (fileslug)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-in-layout-fileslug.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/test/permalink-in-layout-fileslug/index.html"); }); test("Layout from template-data-file that has a permalink (fileslug) Issue #121", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/permalink-data-layout/test.njk", "./test/stubs/", "./dist", dataObj ); let data = await tmpl.getData(); let renderedTmpl = (await getRenderedTmpls(tmpl, data))[0]; t.is(renderedTmpl.templateContent, "Wrapper:Test 1:test"); t.is(await tmpl.getOutputPath(data), "./dist/test/index.html"); }); test("Fileslug in an 11ty.js template Issue #588", async (t) => { let tmpl = await getNewTemplate("./test/stubs/fileslug.11ty.cjs", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let renderedTmpl = (await getRenderedTmpls(tmpl, data))[0]; t.is(renderedTmpl.templateContent, "

fileslug

"); }); test("Local template data file import (without a global data json)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/component/component.njk", "./test/stubs/", "./dist", dataObj ); let data = await tmpl.getData(); t.deepEqual(await dataObj.getLocalDataPaths(tmpl.getInputPath()), [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/component/component.json", "./test/stubs/component/component.11tydata.json", "./test/stubs/component/component.11tydata.mjs", "./test/stubs/component/component.11tydata.cjs", "./test/stubs/component/component.11tydata.js", ]); t.is(data.localdatakey1, "localdatavalue1"); t.is(await renderTemplate(tmpl, data), "localdatavalue1"); }); test("Local template data file import (two subdirectories deep)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/firstdir/seconddir/component.njk", "./test/stubs/", "./dist", dataObj ); t.deepEqual(await dataObj.getLocalDataPaths(tmpl.getInputPath()), [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/firstdir/firstdir.json", "./test/stubs/firstdir/firstdir.11tydata.json", "./test/stubs/firstdir/firstdir.11tydata.mjs", "./test/stubs/firstdir/firstdir.11tydata.cjs", "./test/stubs/firstdir/firstdir.11tydata.js", "./test/stubs/firstdir/seconddir/seconddir.json", "./test/stubs/firstdir/seconddir/seconddir.11tydata.json", "./test/stubs/firstdir/seconddir/seconddir.11tydata.mjs", "./test/stubs/firstdir/seconddir/seconddir.11tydata.cjs", "./test/stubs/firstdir/seconddir/seconddir.11tydata.js", "./test/stubs/firstdir/seconddir/component.json", "./test/stubs/firstdir/seconddir/component.11tydata.json", "./test/stubs/firstdir/seconddir/component.11tydata.mjs", "./test/stubs/firstdir/seconddir/component.11tydata.cjs", "./test/stubs/firstdir/seconddir/component.11tydata.js", ]); }); test("Posts inherits local JSON, layouts", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/posts/post1.njk", "./test/stubs/", "./dist", dataObj ); let localDataPaths = await dataObj.getLocalDataPaths(tmpl.getInputPath()); t.deepEqual(localDataPaths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/posts/posts.json", "./test/stubs/posts/posts.11tydata.json", "./test/stubs/posts/posts.11tydata.mjs", "./test/stubs/posts/posts.11tydata.cjs", "./test/stubs/posts/posts.11tydata.js", "./test/stubs/posts/post1.json", "./test/stubs/posts/post1.11tydata.json", "./test/stubs/posts/post1.11tydata.mjs", "./test/stubs/posts/post1.11tydata.cjs", "./test/stubs/posts/post1.11tydata.js", ]); let localData = await dataObj.getTemplateDirectoryData(tmpl.getInputPath()); t.is(localData.layout, "mylocallayout.njk"); let globalData = await dataObj.getGlobalData(); t.truthy(globalData.pkg); let data = await tmpl.getData(); t.is(localData.layout, "mylocallayout.njk"); t.is( normalizeNewLines((await renderTemplate(tmpl, data)).trim()), `
Post1
` ); }); test("Template and folder name are the same, make sure data imports work ok", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/posts/posts.njk", "./test/stubs/", "./dist", dataObj ); let localDataPaths = await dataObj.getLocalDataPaths(tmpl.getInputPath()); t.deepEqual(localDataPaths, [ "./test/stubs/stubs.json", "./test/stubs/stubs.11tydata.json", "./test/stubs/stubs.11tydata.mjs", "./test/stubs/stubs.11tydata.cjs", "./test/stubs/stubs.11tydata.js", "./test/stubs/posts/posts.json", "./test/stubs/posts/posts.11tydata.json", "./test/stubs/posts/posts.11tydata.mjs", "./test/stubs/posts/posts.11tydata.cjs", "./test/stubs/posts/posts.11tydata.js", ]); let localData = await dataObj.getTemplateDirectoryData(tmpl.getInputPath()); t.is(localData.layout, "mylocallayout.njk"); let globalData = await dataObj.getGlobalData(); t.truthy(globalData.pkg); let data = await tmpl.getData(); t.is(localData.layout, "mylocallayout.njk"); t.is( normalizeNewLines((await renderTemplate(tmpl, data)).trim()), `
Posts
` ); }); test("Clone the template", async (t) => { let tmpl = await getNewTemplate("./test/stubs/default.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let cloned = await tmpl.clone(); let clonedData = await cloned.getData(); t.is(await tmpl.getOutputPath(data), "./dist/default/index.html"); t.is(await cloned.getOutputPath(clonedData), "./dist/default/index.html"); t.is(cloned.extensionMap, tmpl.extensionMap); }); test("getRenderedData() has all the page variables", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); t.truthy(data.page.url); t.is(data.page.url, "/template/"); t.is(data.page.fileSlug, "template"); t.is(data.page.filePathStem, "/template"); t.truthy(data.page.date.getTime()); t.is(data.page.inputPath, "./test/stubs/template.liquid"); t.is(data.page.outputPath, "./dist/template/index.html"); }); test("Issue #603: page.date Liquid", async (t) => { let tmpl = await getNewTemplate("./test/stubs/pagedate.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.truthy(data.page.date); t.truthy(data.page.date.toUTCString()); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), data.page.date.toString()); }); test("Issue #603: page.date Nunjucks", async (t) => { let tmpl = await getNewTemplate("./test/stubs/pagedate.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.truthy(data.page.date); t.truthy(data.page.date.toUTCString()); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), data.page.date.toString()); }); test("Issue #603: page.date.toUTCString() Nunjucks", async (t) => { // Note this is not supported in Liquid let tmpl = await getNewTemplate("./test/stubs/pagedateutc.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.truthy(data.page.date); t.truthy(data.page.date.toUTCString()); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), data.page.date.toUTCString()); }); test("getTemplates() data has all the root variables", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].url, "/template/"); t.is(templates[0].fileSlug, "template"); t.is(templates[0].filePathStem, "/template"); t.truthy(templates[0].date.getTime()); t.is(templates[0].inputPath, "./test/stubs/template.liquid"); t.is(templates[0].outputPath, "./dist/template/index.html"); }); test("getTemplates() data has all the page variables", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let templates = await tmpl.getTemplates(data); t.is(templates[0].data.page.url, "/template/"); t.is(templates[0].data.page.fileSlug, "template"); t.is(templates[0].filePathStem, "/template"); t.truthy(templates[0].data.page.date.getTime()); t.is(templates[0].data.page.inputPath, "./test/stubs/template.liquid"); t.is(templates[0].data.page.outputPath, "./dist/template/index.html"); }); // Warning `getRenderedTemplates()` is a test function now, so this might be a test testing the tests test("getRenderedTemplates() data has all the page variables", async (t) => { let tmpl = await getNewTemplate("./test/stubs/template.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); let templates = await getRenderedTmpls(tmpl, data); t.is(templates[0].data.page.url, "/template/"); t.is(templates[0].data.page.fileSlug, "template"); t.is(templates[0].filePathStem, "/template"); t.truthy(templates[0].data.page.date.getTime()); t.is(templates[0].data.page.inputPath, "./test/stubs/template.liquid"); t.is(templates[0].data.page.outputPath, "./dist/template/index.html"); }); test("getRenderedData() has good slug (empty, index)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/index.liquid", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); t.is(data.page.fileSlug, ""); t.is(data.page.filePathStem, "/index"); }); test("getRenderedData() has good slug", async (t) => { let tmpl = await getNewTemplate("./test/stubs/includer.liquid", "./test/stubs/", "./dist"); let data = await getRenderedData(tmpl); t.is(data.page.fileSlug, "includer"); t.is(data.page.filePathStem, "/includer"); }); test("Override base templating engine from .liquid to njk", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-njk.liquid", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "My Title"); }); test("Override base templating engine from markdown to 11ty.js, then markdown", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/test-override-js-markdown.11ty.cjs", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

This is markdown

"); }); test("Override base templating engine from .liquid to md", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-md.liquid", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

My Title

"); }); test("Override base templating engine from .liquid to njk,md", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-multiple.md", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

My Title

"); }); test("Override base templating engine from .njk to liquid,md", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-multiple2.njk", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

My Title

"); }); test("Override base templating engine from .html to njk", async (t) => { let tmpl = await getNewTemplate("./test/stubs/overrides/test.html", "./test/stubs/", "./dist"); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

My Title

"); }); test("Override base templating engine from .html to (nothing)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-empty.html", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

{{ title }}

"); }); test("Override base templating engine should error with bad string", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-error.njk", "./test/stubs/", "./dist" ); await t.throwsAsync(async () => { await renderTemplate(tmpl, await tmpl.getData()); }); }); test("Override base templating engine (bypasses markdown)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-bypass.md", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "# My Title"); }); test("Override base templating engine to (nothing)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/test-empty.md", "./test/stubs/", "./dist" ); // not parsed t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "# {{ title }}"); }); test("Override base templating engine from .njk to liquid (with a layout that uses njk)", async (t) => { let tmpl = await getNewTemplate("./test/stubs/overrides/layout.njk", "./test/stubs/", "./dist"); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), `

8

`); }); test("Override base templating engine from .njk to nothing (with a layout that uses njk)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/layoutfalse.njk", "./test/stubs/", "./dist" ); t.is( (await renderTemplate(tmpl, await tmpl.getData())).trim(), `

<%= title %>

` ); }); test("Using a markdown source file (with a layout that uses njk), markdown shouldn’t render in layout file", async (t) => { let tmpl = await getNewTemplate("./test/stubs/overrides/test.md", "./test/stubs/", "./dist"); t.is( normalizeNewLines((await renderTemplate(tmpl, await tmpl.getData())).trim()), `# Layout header

My Title

` ); }); test("renderDirect on a markdown file, permalink should not render markdown", async (t) => { let tmpl = await getNewTemplate("./test/stubs/permalink-markdown.md", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is( await tmpl.renderDirect("/news/my-test-file/index.html", {}, true), "/news/my-test-file/index.html" ); t.is(await tmpl.getRawOutputPath(data), "/news/my-test-file/index.html"); }); test("renderDirect on a markdown file, permalink should not render markdown (with variable)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-markdown-var.md", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is( await tmpl.renderDirect("/news/{{ slug }}/index.html", { slug: "my-title" }, true), "/news/my-title/index.html" ); t.is(await tmpl.getRawOutputPath(data), "/news/my-title/index.html"); }); test("renderDirect on a markdown file, permalink should not render markdown (has override)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-markdown-override.md", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is( await tmpl.renderDirect("/news/my-test-file/index.html", {}, true), "/news/my-test-file/index.html" ); t.is(await tmpl.getRawOutputPath(data), "/news/my-test-file/index.html"); }); /* Transforms */ test("Test a transform", async (t) => { t.plan(2); let tmpl = await getNewTemplate( "./test/stubs/template.liquid", "./test/stubs/", "./test/stubs/_site" ); tmpl.setTransforms({ transformName: function (content, outputPath) { t.true(outputPath.endsWith(".html")); return "OVERRIDE BY A TRANSFORM"; } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "OVERRIDE BY A TRANSFORM"); }); // #789: https://github.com/11ty/eleventy/issues/789 test("Test a transform (does it have this.inputPath?)", async (t) => { t.plan(3); let tmpl = await getNewTemplate( "./test/stubs/template.liquid", "./test/stubs/", "./test/stubs/_site" ); tmpl.setTransforms({ transformName: function (content, outputPath) { t.true(outputPath.endsWith(".html")); t.true(!!this.inputPath); return "OVERRIDE BY A TRANSFORM"; } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "OVERRIDE BY A TRANSFORM"); }); test("Test a transform with pages", async (t) => { t.plan(5); let tmpl = await getNewTemplate( "./test/stubs/transform-pages/template.njk", "./test/stubs/", "./test/stubs/_site" ); tmpl.setTransforms({ transformName: function (content, outputPath) { // should run twice, one for each page t.true(content.length > 0); t.true(outputPath.endsWith(".html")); return "OVERRIDE BY A TRANSFORM"; } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "OVERRIDE BY A TRANSFORM"); }); test("Test a transform with a layout", async (t) => { t.plan(3); let tmpl = await getNewTemplate( "./test/stubs-475/transform-layout/transform-layout.njk", "./test/stubs-475/", "./test/stubs-475/_site" ); tmpl.setTransforms({ transformName: function (content, outputPath) { t.is(content, "This is content."); t.true(outputPath.endsWith(".html")); return "OVERRIDE BY A TRANSFORM"; } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "OVERRIDE BY A TRANSFORM"); }); test("Test a single asynchronous transform", async (t) => { t.plan(2); let tmpl = await getNewTemplate( "./test/stubs/template.liquid", "./test/stubs/", "./test/stubs/_site" ); tmpl.setTransforms({ transformName: async function (content, outputPath) { t.true(outputPath.endsWith("template/index.html")); return new Promise((resolve) => { setTimeout(function (str, outputPath) { resolve("OVERRIDE BY A TRANSFORM"); }, 50); }); } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "OVERRIDE BY A TRANSFORM"); }); test("Test multiple asynchronous transforms", async (t) => { t.plan(3); let tmpl = await getNewTemplate( "./test/stubs/template.liquid", "./test/stubs/", "./test/stubs/_site" ); tmpl.setTransforms({ transformName1: async function (content, outputPath) { t.true(outputPath.endsWith("template/index.html")); return new Promise((resolve, reject) => { setTimeout(function (str, outputPath) { resolve("lowercase transform"); }, 50); }); }, // uppercase transformName2: async function (str, outputPath) { t.true(outputPath.endsWith("template/index.html")); return new Promise((resolve, reject) => { setTimeout(function () { resolve(str.toUpperCase()); }, 50); }); } }); let renders = await _testCompleteRender(tmpl); t.is(renders[0], "LOWERCASE TRANSFORM"); }); test("Test a linter", async (t) => { t.plan(4); let tmpl = await getNewTemplate( "./test/stubs/transform-pages/template.njk", "./test/stubs/", "./test/stubs/_site" ); tmpl.addLinter(function (str, inputPath, outputPath) { t.true(inputPath.endsWith("template.njk")); t.true(outputPath.endsWith("index.html")); }); await _testCompleteRender(tmpl); }); test("Front Matter Tags (Single)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/templatetest-frontmatter/single.njk", "./test/stubs/", "dist" ); let { data: frontmatter } = await tmpl.getFrontMatterData(); t.deepEqual(frontmatter.tags, ["single-tag"]); let fulldata = await tmpl.getData(); t.deepEqual(fulldata.tags, ["single-tag"]); let pages = await getRenderedTmpls(tmpl, fulldata); t.is(pages[0].templateContent.trim(), "Has single-tag"); }); test("Front Matter Tags (Multiple)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/templatetest-frontmatter/multiple.njk", "./test/stubs/", "dist" ); let { data: frontmatter } = await tmpl.getFrontMatterData(); t.deepEqual(frontmatter.tags, ["multi-tag", "multi-tag-2"]); let fulldata = await tmpl.getData(); t.deepEqual(fulldata.tags, ["multi-tag", "multi-tag-2"]); let pages = await getRenderedTmpls(tmpl, fulldata); t.is(pages[0].templateContent.trim(), "Has multi-tag-2"); }); test("Front matter date with quotes (liquid), issue #258", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/frontmatter-date/test.liquid", "./test/stubs/", "dist" ); let data = await tmpl.getData(); t.is(data.mydate.toISOString(), "2009-04-15T11:34:34.000Z"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "2009-04-15"); }); test("Front matter date with quotes (njk), issue #258", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/frontmatter-date/test.njk", "./test/stubs/", "dist" ); let data = await tmpl.getData(); t.is(data.mydate.toISOString(), "2009-04-15T00:34:34.000Z"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "2009-04-15T00:34:34.000Z"); }); test("Data Cascade (Deep merge)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test", output: "dist" }, function(cfg) { // Default changed in 1.0 // cfg.setDataDeepMerge(true); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/data-cascade/template.njk", "./test/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.deepEqual(Object.keys(data).sort(), [ "datafile", "eleventy", "frontmatter", "page", "parent", "pkg", "tags", ]); t.deepEqual(Object.keys(data.parent).sort(), ["child", "datafile", "frontmatter"]); t.is(data.parent.child, -2); }); test("Data Cascade Tag Merge (Deep merge)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist" }, function(cfg) { // Default changed in 1.0 // cfg.setDataDeepMerge(true); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/data-cascade/template.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.deepEqual(data.tags.sort(), ["tagA", "tagB", "tagC", "tagD"]); }); test("Data Cascade Tag Merge (Deep Merge - Deduplication)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist" }, function(cfg) { // Default changed in 1.0 // cfg.setDataDeepMerge(true); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/data-cascade/template.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.deepEqual(data.tags.sort(), ["tagA", "tagB", "tagC", "tagD"]); }); test('Local data inherits tags string ([tags] vs "tags") Deep Merge', async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist" }, function(cfg) { // Default changed in 1.0 // cfg.setDataDeepMerge(true); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); await dataObj.getGlobalData(); let tmpl = await getNewTemplate( "./test/stubs/local-data-tags/component.njk", "./test/stubs/", "./dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.deepEqual(data.tags.sort(), ["tag1", "tag2", "tag3"]); }); test("Throws a Premature Template Content Error (njk)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.njk", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplates(data); let error = t.throws(() => { mapEntries[0].templateContent; }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Throws a Premature Template Content Error from rendering (njk)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.njk", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplateMapEntries(data); let pageEntries = await tmpl.getTemplates({ page: {}, sample: { get templateContent() { throw new TemplateContentPrematureUseError( "Tried to use templateContent too early (test.njk)" ); }, }, }); let error = await t.throwsAsync(async () => { await tmpl.renderPageEntry(pageEntries[0]); }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Throws a Premature Template Content Error (liquid)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.liquid", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplates(data); let error = t.throws(() => { mapEntries[0].templateContent; }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Throws a Premature Template Content Error (11ty.js)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.11ty.cjs", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplates(data); let error = t.throws(() => { mapEntries[0].templateContent; }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Throws a Premature Template Content Error (md)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplates(data); let error = t.throws(() => { mapEntries[0].templateContent; }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Throws a Premature Template Content Error from rendering (md)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/prematureTemplateContent/test.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await tmpl.getTemplateMapEntries(data); let pageEntries = await tmpl.getTemplates({ page: {}, sample: { get templateContent() { throw new TemplateContentPrematureUseError( "Tried to use templateContent too early (test.md)" ); }, }, }); let error = await t.throwsAsync(async () => { await tmpl.renderPageEntry(pageEntries[0]); }); t.is(EleventyErrorUtil.isPrematureTemplateContentError(error), true); }); test("Issue 413 weird date format", async (t) => { let tmpl = await getNewTemplate( "./test/stubs-413/date-frontmatter.md", "./test/stubs-413/", "./dist" ); await t.throwsAsync(async function () { await tmpl.getData(); }, { message: "Data cascade value for `date` (2019-03-13 20:18:42 +0000) is invalid for ./test/stubs-413/date-frontmatter.md" }); }); test("Custom Front Matter Parsing Options", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig, ); let frontmatter = await tmpl._testGetFrontMatter(); t.is(frontmatter.data.front, "hello"); t.is(frontmatter.excerpt.trim(), "This is an excerpt."); t.is( normalizeNewLines(frontmatter.content.trim()), `This is an excerpt. This is content.` ); let fulldata = await tmpl.getData(); t.is(fulldata.page.excerpt.trim(), "This is an excerpt."); }); test("Custom Front Matter Parsing Options (using alias)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, excerpt_alias: "my_excerpt", }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig, ); let frontmatter = await tmpl._testGetFrontMatter(); t.is(frontmatter.data.front, "hello"); t.is( normalizeNewLines(frontmatter.content.trim()), `This is an excerpt. This is content.` ); let fulldata = await tmpl.getData(); t.is(fulldata.my_excerpt.trim(), "This is an excerpt."); }); test("Custom Front Matter Parsing Options (no newline before excerpt separator)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template-newline1.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig, ); let frontmatter = await tmpl._testGetFrontMatter(); t.is(frontmatter.data.front, "hello"); t.is(frontmatter.excerpt.trim(), "This is an excerpt."); t.is( normalizeNewLines(frontmatter.content.trim()), `This is an excerpt. This is content.` ); let fulldata = await tmpl.getData(); t.is(fulldata.page.excerpt.trim(), "This is an excerpt."); }); test("Custom Front Matter Parsing Options (no newline after excerpt separator)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template-newline3.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig, ); let frontmatter = await tmpl._testGetFrontMatter(); t.is( normalizeNewLines(frontmatter.content.trim()), `This is an excerpt. This is content.` ); }); test("Custom Front Matter Parsing Options (no newlines before or after excerpt separator)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template-newline2.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig ); let frontmatter = await tmpl._testGetFrontMatter(); t.is(frontmatter.content.trim(), "This is an excerpt.This is content."); }); test("Custom Front Matter Parsing Options (html comment separator)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({}, function(cfg) { cfg.setFrontMatterParsingOptions({ excerpt: true, excerpt_separator: "", }) }); let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template-excerpt-comment.njk", "./test/stubs/", "./dist", undefined, undefined, eleventyConfig ); let frontmatter = await tmpl._testGetFrontMatter(); t.is(frontmatter.data.front, "hello"); t.is(frontmatter.excerpt.trim(), "This is an excerpt."); t.is( normalizeNewLines(frontmatter.content.trim()), `This is an excerpt. This is content.` ); let fulldata = await tmpl.getData(); t.is(fulldata.page.excerpt.trim(), "This is an excerpt."); }); test("Custom Front Matter Parsing Options (using TOML)", async (t) => { // Currently fails on Windows, needs https://github.com/jonschlinkert/gray-matter/issues/92 let tmpl = await getNewTemplate( "./test/stubs/custom-frontmatter/template-toml.njk", "./test/stubs/", "./dist" ); tmpl.config.frontMatterParsingOptions = { engines: { toml: TOML.parse.bind(TOML), }, }; let frontmatter = await tmpl._testGetFrontMatter(); t.deepEqual(frontmatter.data, { front: "hello", }); t.is(frontmatter.content.trim(), "This is content."); let fulldata = await tmpl.getData(); t.is(fulldata.front, "hello"); }); test("global variable with dashes Issue #567 (liquid)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/global-dash-variable.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(data["is-it-tasty"], "Yes"); let pages = await getRenderedTmpls(tmpl, data); t.is(pages[0].templateContent.trim(), "Yes"); }); test("Issue #446: Layout has a permalink with a different template language than content", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/layout-permalink-difflang/test.md", "./test/stubs/layout-permalink-difflang/", "dist" ); let data = await tmpl.getData(); // this call is needed for page data to be added let pages = await getRenderedTmpls(tmpl, data); t.is(data.permalink, "/{{ page.fileSlug }}/"); t.is(data.page.url, "/test/"); }); test("Get Layout Chain", async (t) => { let tmpl = await getNewTemplate( "./test/stubs-incremental/layout-chain/test.njk", "./test/stubs-incremental/layout-chain/", "./dist" ); let data = await tmpl.getData(); let layout = tmpl.getLayout(data.layout); t.deepEqual(await layout.getLayoutChain(), [ "./test/stubs-incremental/layout-chain/_includes/base.njk", "./test/stubs-incremental/layout-chain/_includes/parent.njk", ]); }); test("Engine Singletons", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/engine-singletons", output: "dist" } }); let map = new EleventyExtensionMap(eleventyConfig); map.engineManager = new TemplateEngineManager(eleventyConfig); map.setFormats(["njk"]); let tmpl1 = await getNewTemplate( "./test/stubs/engine-singletons/first.njk", "./test/stubs/engine-singletons/", "./dist", null, map, eleventyConfig ); let tmpl2 = await getNewTemplate( "./test/stubs/engine-singletons/second.njk", "./test/stubs/engine-singletons/", "./dist", null, map, eleventyConfig ); t.deepEqual(tmpl1.engine, tmpl2.engine); }); test("Make sure layout cache takes new changes during watch (nunjucks)", async (t) => { let filePath = "./test/stubs-layout-cache/_includes/include-script-1.js"; fs.writeFileSync(filePath, `alert("hi");`, "utf8"); let tmpl = await getNewTemplate( "./test/stubs-layout-cache/test.njk", "./test/stubs-layout-cache/", "./dist" ); let data = await tmpl.getData(); t.is((await renderTemplate(tmpl, data)).trim(), ''); fs.writeFileSync(filePath, `alert("bye");`, "utf8"); tmpl.config.events.emit("eleventy#templateModified", filePath); t.is((await renderTemplate(tmpl, data)).trim(), ''); }); test("Make sure layout cache takes new changes during watch (liquid)", async (t) => { let filePath = "./test/stubs-layout-cache/_includes/include-script-2.js"; fs.writeFileSync(filePath, `alert("hi");`, "utf8"); let tmpl = await getNewTemplate( "./test/stubs-layout-cache/test.liquid", "./test/stubs-layout-cache/", "./dist" ); let data = await tmpl.getData(); t.is((await renderTemplate(tmpl, data)).trim(), ''); fs.writeFileSync(filePath, `alert("bye");`, "utf8"); // Trigger that the file has changed tmpl.eleventyConfig.setPreviousBuildModifiedFile(filePath); t.is((await renderTemplate(tmpl, data)).trim(), ''); }); test("Add Extension via Configuration (txt file)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs", output: "dist" }, function(cfg) { cfg.addExtension("txt", { isIncrementalMatch: function (incrementalFilePath) { // do some kind of check return this.inputPath === incrementalFilePath; }, compile: function (str, inputPath) { // plaintext return function (data) { return str; }; }, }); }); let map = new EleventyExtensionMap(eleventyConfig); map.engineManager = new TemplateEngineManager(eleventyConfig); map.setFormats([]); let tmpl = await getNewTemplate( "./test/stubs/default.txt", "./test/stubs/", "./dist", null, map, eleventyConfig ); let extensions = tmpl.getExtensionEntries(); t.deepEqual(extensions[0].key, "txt"); t.deepEqual(extensions[0].extension, "txt"); t.truthy(tmpl.isFileRelevantToThisTemplate("./test/stubs/default.txt")); t.falsy(tmpl.isFileRelevantToThisTemplate("./test/stubs/default2.txt")); t.falsy(tmpl.isFileRelevantToThisTemplate("./test/stubs/default.njk")); t.truthy( tmpl.isFileRelevantToThisTemplate("./test/stubs/default.txt", { isFullTemplate: true, }) ); t.falsy( tmpl.isFileRelevantToThisTemplate("./test/stubs/default2.txt", { isFullTemplate: true, }) ); t.falsy( tmpl.isFileRelevantToThisTemplate("./test/stubs/default.njk", { isFullTemplate: true, }) ); }); test("permalink object with build", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-build/permalink-build.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); t.is(await tmpl.getRawOutputPath(data), "/url/index.html"); t.is(await tmpl.getOutputHref(data), "/url/"); }); test("permalink object _getLink", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-nobuild/permalink-nobuild.md", "./test/stubs/", "./test/stubs/_site" ); let link2 = await tmpl._getLink({ permalink: { build: "/build/", }, }); t.is(await link2.toOutputPath(), "/build/index.html"); t.is(await link2.toHref(), "/build/"); }); test("permalink object _getLink (array of invalid, previously serverless URLs)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-nobuild/permalink-nobuild.md", "./test/stubs/", "./test/stubs/_site" ); // Array of URLs is supported let link = await tmpl._getLink({ test: "a", permalink: { someotherkey: [], }, }); t.is(await link.toOutputPath(), false); t.is(await link.toHref(), false); }); test("Permalink is an object but an empty object (inherit default behavior)", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-empty-object/empty-object.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let outputHref = await tmpl.getOutputHref(data); t.is(outputHref, "/permalink-empty-object/empty-object/"); let outputLink = await tmpl.getRawOutputPath(data); t.is(outputLink, "permalink-empty-object/empty-object/index.html"); let outputPath = await tmpl.getOutputPath(data); t.is(outputPath, "./test/stubs/_site/permalink-empty-object/empty-object/index.html"); let { href, rawPath, path } = await tmpl.getOutputLocations(data); t.is(href, "/permalink-empty-object/empty-object/"); t.is(rawPath, "permalink-empty-object/empty-object/index.html"); t.is(path, "./test/stubs/_site/permalink-empty-object/empty-object/index.html"); }); test("eleventyComputed returns permalink object Issue #1898", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/stubs-computed-permalink/eleventycomputed-object.11ty.cjs", "./test/stubs/stubs-computed-permalink/", "./test/stubs/stubs-computed-permalink/_site" ); let data = await tmpl.getData(); let [page] = await tmpl.getTemplates(data); t.is(page.url, "/i18n/en/"); t.is(page.outputPath, "./test/stubs/stubs-computed-permalink/_site/i18n/en/index.html"); }); test("eleventyComputed returns nested permalink object Issue #1898", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/stubs-computed-permalink/eleventycomputed-nested-object.11ty.cjs", "./test/stubs/stubs-computed-permalink/", "./test/stubs/stubs-computed-permalink/_site" ); let data = await tmpl.getData(); let [page] = await tmpl.getTemplates(data); t.is(page.url, "/i18n/en/"); t.is(page.outputPath, "./test/stubs/stubs-computed-permalink/_site/i18n/en/index.html"); }); test("eleventyComputed returns permalink object using permalink string (with replace) Issue #1898", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/stubs-computed-permalink/eleventycomputed-object-replace.11ty.cjs", "./test/stubs/stubs-computed-permalink/", "./test/stubs/stubs-computed-permalink/_site" ); let data = await tmpl.getData(); let [page] = await tmpl.getTemplates(data); t.is(page.url, "/i18n/en/"); t.is(page.outputPath, "./test/stubs/stubs-computed-permalink/_site/i18n/en/index.html"); }); test("page.templateSyntax works with templateEngineOverride", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/overrides/page-templatesyntax.md", "./test/stubs/", "./dist" ); t.is((await renderTemplate(tmpl, await tmpl.getData())).trim(), "

njk,md

"); }); // Inspired by https://github.com/11ty/eleventy/pull/1691 test("Error messaging, returning literals (not objects) from custom data extension", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs-1691", output: "dist" }, function(cfg) { cfg.addDataExtension("txt", { parser: (s) => s, }); }); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs-1691/template.njk", "./test/stubs-1691/", "dist", dataObj, null, eleventyConfig ); let data = await tmpl.getData(); t.is(data.str, "Testing"); }); ================================================ FILE: test/TemplateTest_Permalink.js ================================================ import test from "ava"; import fs from "fs"; import TemplateData from "../src/Data/TemplateData.js"; import getNewTemplate from "./_getNewTemplateForTests.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; async function writeMapEntries(mapEntries) { let promises = []; for (let entry of mapEntries) { if (entry.template.behavior.isWriteable()) { promises.push(entry.template._write(entry.outputPath, entry.templateContent)); } } return Promise.all(promises); } async function getTemplateMapEntriesWithContent(template, data) { let entries = await template.getTemplateMapEntries(data); return Promise.all( entries.map(async (entry) => { entry._pages = await entry.template.getTemplates(entry.data); await Promise.all( entry._pages.map(async (page) => { page.templateContent = await page.template.renderPageEntryWithoutLayout(page); return page; }) ); return entry; }) ); } test("permalink: false", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-false/test.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await getTemplateMapEntriesWithContent(tmpl, data); for (let entry of mapEntries) { t.is(entry.template.behavior.isWriteable(), false); t.is(entry.data.page.url, false); t.is(entry.data.page.outputPath, false); } await writeMapEntries(mapEntries); // Input file exists (sanity check for paths) t.is(fs.existsSync("./test/stubs/permalink-false/"), true); t.is(fs.existsSync("./test/stubs/permalink-false/test.md"), true); // Output does not exist t.is(fs.existsSync("./test/stubs/_site/permalink-false/"), false); t.is(fs.existsSync("./test/stubs/_site/permalink-false/test/"), false); t.is(fs.existsSync("./test/stubs/_site/permalink-false/test/index.html"), false); }); test("permalink: false inside of eleventyComputed, Issue #1754", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-false-computed/test.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); let mapEntries = await getTemplateMapEntriesWithContent(tmpl, data); for (let entry of mapEntries) { t.is(entry.template.behavior.isWriteable(), false); t.is(entry.data.page.url, false); t.is(entry.data.page.outputPath, false); } await writeMapEntries(mapEntries); // Input file exists (sanity check for paths) t.is(fs.existsSync("./test/stubs/permalink-false-computed/"), true); t.is(fs.existsSync("./test/stubs/permalink-false-computed/test.md"), true); // Output does not exist t.is(fs.existsSync("./test/stubs/_site/permalink-false-computed/"), false); t.is(fs.existsSync("./test/stubs/_site/permalink-false-computed/test/"), false); t.is(fs.existsSync("./test/stubs/_site/permalink-false-computed/test/index.html"), false); }); test("permalink: true", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalink-true/permalink-true.md", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); await t.throwsAsync(async () => { await tmpl.getRawOutputPath(data); }); }); test("Disable dynamic permalinks", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/dynamic-permalink/test.njk", "./test/stubs/", "./test/stubs/_site" ); let data = await tmpl.getData(); t.is(await tmpl.getRawOutputPath(data), "/{{justastring}}/index.html"); t.is(await tmpl.getOutputHref(data), "/{{justastring}}/"); // TODO https://github.com/11ty/eleventy-plugin-webc/issues/32 // t.is(data.page.url, "/{{justastring}}/") }); test("Permalink with variables!", async (t) => { let tmpl = await getNewTemplate("./test/stubs/permalinkdata.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/slug-candidate/index.html"); }); test("Permalink with variables and JS front matter!", async (t) => { let tmpl = await getNewTemplate("./test/stubs/permalinkdata-jsfn.njk", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/slug/index.html"); }); test("Use a JavaScript function for permalink in any template language", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/permalinkdata-jspermalinkfn.njk", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/slug/index.html"); }); test("Permalink with dates!", async (t) => { let tmpl = await getNewTemplate("./test/stubs/permalinkdate.liquid", "./test/stubs/", "./dist"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/2016/01/01/index.html"); }); test.skip("Permalink with dates on file name regex!", async (t) => { let tmpl = await getNewTemplate( "./test/stubs/2016-02-01-permalinkdate.liquid", "./test/stubs/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/2016/02/01/index.html"); }); test("Reuse permalink in directory specific data file", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "dist" } }); let dataObj = new TemplateData(eleventyConfig); dataObj.setProjectUsingEsm(true); let tmpl = await getNewTemplate( "./test/stubs/reuse-permalink/test1.liquid", "./test/stubs/", "./dist", dataObj ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/2016/01/01/index.html"); }); test("Using slugify filter!", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/test.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/slug-love-candidate-lyublyu/index.html"); }); test("Using slugify filter with comma and apostrophe", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/comma.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/hi-i-m-zach/index.html"); }); test("Using slug filter with options params", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/slug-options.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/hi_i_am_zach/index.html"); }); test("Using slugify filter with options params", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/slugify-options.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/hi-i-m-z-ach/index.html"); }); test("Using slugify filter with a number #854", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/slugify-number.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/1/index.html"); }); test("Using slug filter with a number #854", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/slug-number.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/1/index.html"); }); test.skip("Using slugify filter with multibyte", async (t) => { let tmpl = await getNewTemplate( "./test/slugify-filter/multibyte.njk", "./test/slugify-filter/", "./dist" ); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./dist/subdir/test-猫/index.html"); }); ================================================ FILE: test/TemplateWriterTest.js ================================================ import test from "ava"; import fs from "fs"; import { glob } from "tinyglobby"; import path from "path"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { isTypeScriptSupported } from "../src/Util/FeatureTests.cjs"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; import { getRenderedTemplates as getRenderedTmpls } from "./_getRenderedTemplates.js"; import { getTemplateConfigInstance, getTemplateConfigInstanceCustomCallback, getTemplateWriterInstance, deleteDirectory } from "./_testHelpers.js"; // TODO make sure if output is a subdir of input dir that they don’t conflict. test("Output is a subdir of input", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTest", output: "test/stubs/writeTest/_writeTestSite" } }); let { templateWriter: tw, eleventyFiles: evf } = getTemplateWriterInstance(["liquid", "md"], eleventyConfig); let files = await glob(evf.getFileGlobs()); t.deepEqual(evf.getRawFiles(), ["./test/stubs/writeTest/**/*.{liquid,md}"]); t.true(files.length > 0); let { template: tmpl } = tw._createTemplate(files[0]); t.is(tmpl.inputDir, "./test/stubs/writeTest/"); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./test/stubs/writeTest/_writeTestSite/test/index.html"); }); test("_createTemplateMap", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTest", output: "test/stubs/writeTest/_writeTestSite" } }); let { templateWriter: tw } = getTemplateWriterInstance(["liquid", "md"], eleventyConfig); let paths = await tw._getAllPaths(); t.true(paths.length > 0); t.is(paths[0], "./test/stubs/writeTest/test.md"); let templateMap = await tw._createTemplateMap(paths); let map = templateMap.getMap(); t.true(map.length > 0); t.truthy(map[0].template); t.truthy(map[0].data); }); test("_createTemplateMap (no leading dot slash)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTest", output: "test/stubs/_writeTestSite" } }); let { templateWriter: tw } = getTemplateWriterInstance(["liquid", "md"], eleventyConfig); let paths = await tw._getAllPaths(); t.true(paths.length > 0); t.is(paths[0], "./test/stubs/writeTest/test.md"); }); test("_testGetCollectionsData", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/collection", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.post.length, 2); t.is(collectionsData.cat.length, 2); t.is(collectionsData.dog.length, 1); }); // TODO remove this (used by other test things) test("_testGetAllTags", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/collection", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let tags = templateMap._testGetAllTags(); t.deepEqual(tags.sort(), ["cat", "dog", "post", "office"].sort()); }); test("Collection of files sorted by date", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/dates", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance( ["md"], eleventyConfig, ); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.dateTestTag.length, 6); }); test("__testGetCollectionsData with custom collection (ascending)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(config) { config.addCollection("customPostsAsc", function (collection) { return collection.getFilteredByTag("post").sort(function (a, b) { return a.date - b.date; }); }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.customPostsAsc.length, 2); t.is(path.parse(collectionsData.customPostsAsc[0].inputPath).base, "test1.md"); t.is(path.parse(collectionsData.customPostsAsc[1].inputPath).base, "test2.md"); }); test("__testGetCollectionsData with custom collection (descending)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(eleventyConfig) { eleventyConfig.addCollection("customPosts", function (collection) { return collection.getFilteredByTag("post").sort(function (a, b) { return b.date - a.date; }); }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.customPosts.length, 2); t.is(path.parse(collectionsData.customPosts[0].inputPath).base, "test2.md"); t.is(path.parse(collectionsData.customPosts[1].inputPath).base, "test1.md"); }); test("__testGetCollectionsData with custom collection (filter only to markdown input)", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(config) { config.addCollection("onlyMarkdown", function (collection) { return collection.getAllSorted().filter(function (item) { let extension = item.inputPath.split(".").pop(); return extension === "md"; }); }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.onlyMarkdown.length, 2); t.is(path.parse(collectionsData.onlyMarkdown[0].inputPath).base, "test1.md"); t.is(path.parse(collectionsData.onlyMarkdown[1].inputPath).base, "test2.md"); }); test("Pagination with a Collection", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/paged/collection", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.tag1.length, 3); t.is(collectionsData.pagingtag.length, 1); let mapEntry = templateMap.getMapEntryForInputPath("./test/stubs/paged/collection/main.njk"); t.truthy(mapEntry); t.is(mapEntry.inputPath, "./test/stubs/paged/collection/main.njk"); t.is(mapEntry._pages.length, 2); t.is(mapEntry._pages[0].outputPath, "./test/stubs/_site/main/index.html"); t.is(mapEntry._pages[1].outputPath, "./test/stubs/_site/main/1/index.html"); t.is(mapEntry._pages[0].templateContent.trim(), "
  1. /test1/
  2. /test2/
"); t.is(mapEntry._pages[1].templateContent.trim(), "
  1. /test3/
"); }); test("Pagination with a Collection from another Paged Template", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/paged/cfg-collection-tag-cfg-collection", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.tag1.length, 3); t.is(collectionsData.pagingtag.length, 2); let map1 = templateMap.getMapEntryForInputPath( "./test/stubs/paged/cfg-collection-tag-cfg-collection/paged-main.njk", ); t.is(map1._pages[0].templateContent.trim(), "
  1. /test1/
  2. /test2/
"); t.is(map1._pages[1].templateContent.trim(), "
  1. /test3/
"); let map2 = templateMap.getMapEntryForInputPath( "./test/stubs/paged/cfg-collection-tag-cfg-collection/paged-downstream.njk", ); t.is(map2._pages[0].templateContent.trim(), "
  1. /paged-main/
"); t.is(map2._pages[1].templateContent.trim(), "
  1. /paged-main/1/
"); }); test("Pagination with a Collection (apply all pages to collections)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/paged/collection-apply-to-all", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.tag1.length, 3); t.is(collectionsData.pagingtag.length, 2); let mapEntry = templateMap.getMapEntryForInputPath( "./test/stubs/paged/collection-apply-to-all/main.njk", ); t.truthy(mapEntry); t.is(mapEntry.inputPath, "./test/stubs/paged/collection-apply-to-all/main.njk"); let { template: mainTmpl } = tw._createTemplate( "./test/stubs/paged/collection-apply-to-all/main.njk", ); let data = await mainTmpl.getData(); let outputPath = await mainTmpl.getOutputPath(data); t.is(outputPath, "./test/stubs/_site/main/index.html"); let templates = await getRenderedTmpls(mapEntry.template, mapEntry.data); t.is(templates.length, 2); t.is( await templates[0].template.getOutputPath(templates[0].data), "./test/stubs/_site/main/index.html", ); t.is(templates[0].outputPath, "./test/stubs/_site/main/index.html"); t.is( await templates[1].template.getOutputPath(templates[1].data), "./test/stubs/_site/main/1/index.html", ); t.is(templates[1].outputPath, "./test/stubs/_site/main/1/index.html"); // test content t.is(templates[0].templateContent.trim(), "
  1. /test1/
  2. /test2/
"); t.is(templates[1].templateContent.trim(), "
  1. /test3/
"); }); test("Use a collection inside of a template", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/collection-template", output: "test/stubs/collection-template/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["liquid"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.dog.length, 1); let mapEntry = templateMap.getMapEntryForInputPath( "./test/stubs/collection-template/template.liquid", ); t.truthy(mapEntry); t.is(mapEntry.inputPath, "./test/stubs/collection-template/template.liquid"); let { template: mainTmpl } = tw._createTemplate( "./test/stubs/collection-template/template.liquid", ); let data = await mainTmpl.getData(); let outputPath = await mainTmpl.getOutputPath(data); t.is(outputPath, "./test/stubs/collection-template/_site/template/index.html"); let templates = await getRenderedTmpls(mapEntry.template, mapEntry.data); // test content t.is( normalizeNewLines(templates[0].templateContent.trim()), `Layout Template All 2 templates Template 1 dog`, ); }); test("Use a collection inside of a layout", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/collection-layout", output: "test/stubs/collection-layout/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["liquid"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.dog.length, 1); let mapEntry = templateMap.getMapEntryForInputPath( "./test/stubs/collection-layout/template.liquid", ); t.truthy(mapEntry); t.is(mapEntry.inputPath, "./test/stubs/collection-layout/template.liquid"); let { template: mainTmpl } = tw._createTemplate("./test/stubs/collection-layout/template.liquid"); let data = await mainTmpl.getData(); let outputPath = await mainTmpl.getOutputPath(data); t.is(outputPath, "./test/stubs/collection-layout/_site/template/index.html"); let templates = await getRenderedTmpls(mapEntry.template, mapEntry.data); // test content t.is( normalizeNewLines(templates[0].templateContent.trim()), `Layout Template All 2 templates Layout 1 dog`, ); }); test("Glob Watcher Files with Passthroughs", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs", output: "test/stubs/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk", "png"], eleventyConfig); t.deepEqual(tw.getPassthroughGlobs(), ["./test/stubs/**/*.png"]); }); test("Pagination and TemplateContent", async (t) => { deleteDirectory("./test/stubs/pagination-templatecontent/_site/"); let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/pagination-templatecontent", output: "test/stubs/pagination-templatecontent/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk", "md"], eleventyConfig); tw.setVerboseOutput(false); await tw.write(); let content = fs.readFileSync("./test/stubs/pagination-templatecontent/_site/index.html", "utf8"); t.is( content.trim(), `

Post 1

Post 2

`, ); deleteDirectory("./test/stubs/pagination-templatecontent/_site/"); }); test("Custom collection returns array", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(config) { config.addCollection("returnAllInputPaths", function (collection) { return collection.getAllSorted().map(function (item) { return item.inputPath; }); }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.returnAllInputPaths.length, 2); t.is(path.parse(collectionsData.returnAllInputPaths[0]).base, "test1.md"); t.is(path.parse(collectionsData.returnAllInputPaths[1]).base, "test2.md"); }); test("Custom collection returns a string", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(config) { config.addCollection("returnATestString", function () { return "test"; }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.returnATestString, "test"); }); test("Custom collection returns an object", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/collection2", output: "test/stubs/_site" }, function(config) { config.addCollection("returnATestObject", function () { return { test: "value" }; }); }); let { templateWriter: tw } = getTemplateWriterInstance(["md"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.deepEqual(collectionsData.returnATestObject, { test: "value" }); }); test("fileSlug should exist in a collection", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/collection-slug", output: "test/stubs/collection-slug/_site" } }); let { templateWriter: tw } = getTemplateWriterInstance(["njk"], eleventyConfig); let paths = await tw._getAllPaths(); let templateMap = await tw._createTemplateMap(paths); let collectionsData = await templateMap._testGetCollectionsData(); t.is(collectionsData.dog.length, 1); let mapEntry = templateMap.getMapEntryForInputPath("./test/stubs/collection-slug/template.njk"); t.truthy(mapEntry); t.is(mapEntry.inputPath, "./test/stubs/collection-slug/template.njk"); let templates = await getRenderedTmpls(mapEntry.template, mapEntry.data); t.is(templates[0].templateContent.trim(), "fileSlug:/dog1/:dog1"); }); test("Write Test 11ty.js", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTestJS", output: "test/stubs/_writeTestJSSite" } }); let { templateWriter: tw, eleventyFiles: evf } = getTemplateWriterInstance(["11ty.js"], eleventyConfig); let files = await glob(evf.getFileGlobs()); t.deepEqual(evf.getRawFiles(), [`./test/stubs/writeTestJS/**/*.{11ty.js,11ty.cjs,11ty.mjs${isTypeScriptSupported() ? ",11ty.ts,11ty.cts,11ty.mts" : ""}}`]); t.deepEqual(files, ["test/stubs/writeTestJS/test.11ty.cjs"]); let { template: tmpl } = tw._createTemplate(files[0]); let data = await tmpl.getData(); t.is(await tmpl.getOutputPath(data), "./test/stubs/_writeTestJSSite/test/index.html"); }); test.skip("Markdown with alias", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTestMarkdown", output: "test/stubs/_writeTestMarkdownSite" } }); let map = new EleventyExtensionMap(eleventyConfig); map.setFormats(["md"]); map.config = { templateExtensionAliases: { markdown: "md", }, }; let { templateWriter: tw, eleventyFiles: evf } = getTemplateWriterInstance(["md"], eleventyConfig); evf._setExtensionMap(map); evf.init(); let files = await glob(evf.getFileGlobs()); t.deepEqual(evf.getRawFiles(), [ "./test/stubs/writeTestMarkdown/**/*.md", "./test/stubs/writeTestMarkdown/**/*.markdown", ]); t.true(files.indexOf("./test/stubs/writeTestMarkdown/sample.md") > -1); t.true(files.indexOf("./test/stubs/writeTestMarkdown/sample2.markdown") > -1); let { template: tmpl } = tw._createTemplate(files[0]); tmpl._setExtensionMap(map); t.is(await tmpl.getOutputPath(), "./test/stubs/_writeTestMarkdownSite/sample/index.html"); let { template: tmpl2 } = tw._createTemplate(files[1]); tmpl2._setExtensionMap(map); t.is(await tmpl2.getOutputPath(), "./test/stubs/_writeTestMarkdownSite/sample2/index.html"); }); test.skip("JavaScript with alias", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/stubs/writeTestJS", output: "test/stubs/_writeTestJSSite" } }); let map = new EleventyExtensionMap(eleventyConfig); map.setFormats(["11ty.js"]); map.config = { templateExtensionAliases: { js: "11ty.js", }, }; let { templateWriter: tw, eleventyFiles: evf } = getTemplateWriterInstance(["11ty.js"], eleventyConfig); evf._setExtensionMap(map); evf.init(); let files = await glob(evf.getFileGlobs()); t.deepEqual( evf.getRawFiles().sort(), ["./test/stubs/writeTestJS/**/*.11ty.js", "./test/stubs/writeTestJS/**/*.js"].sort(), ); t.deepEqual( files.sort(), ["./test/stubs/writeTestJS/sample.js", "./test/stubs/writeTestJS/test.11ty.js"].sort(), ); let { template: tmpl } = tw._createTemplate(files[0]); t.is(await tmpl.getOutputPath(), "./test/stubs/_writeTestJSSite/test/index.html"); }); test("Passthrough file output", async (t) => { deleteDirectory("./test/stubs/template-passthrough/_site/"); let eleventyConfig = await getTemplateConfigInstanceCustomCallback({ input: "test/stubs/template-passthrough", output: "test/stubs/template-passthrough/_site" }, function(cfg){ cfg.addPassthroughCopy("./test/stubs/template-passthrough/static"); cfg.addPassthroughCopy({ "./test/stubs/template-passthrough/static/": "./", }); cfg.addPassthroughCopy({ "./test/stubs/template-passthrough/static/**/*": "./all/", }); cfg.addPassthroughCopy({ "./test/stubs/template-passthrough/static/**/*.js": "./js/", }); // cfg.passthroughCopies = { // "./test/stubs/template-passthrough/static": { // outputPath: true, // }, // "./test/stubs/template-passthrough/static/": { // outputPath: "./", // }, // "./test/stubs/template-passthrough/static/**/*": { // outputPath: "./all/", // }, // "./test/stubs/template-passthrough/static/**/*.js": { // outputPath: "./js/", // }, // }; }); let { templateWriter: tw } = getTemplateWriterInstance(["njk", "md"], eleventyConfig); await tw.write(); const output = [ "./test/stubs/template-passthrough/_site/static/nested/test-nested.css", "./test/stubs/template-passthrough/_site/all/test.js", "./test/stubs/template-passthrough/_site/all/test.css", "./test/stubs/template-passthrough/_site/all/test-nested.css", "./test/stubs/template-passthrough/_site/js/", "./test/stubs/template-passthrough/_site/js/test.js", "./test/stubs/template-passthrough/_site/nested/", "./test/stubs/template-passthrough/_site/nested/test-nested.css", "./test/stubs/template-passthrough/_site/test.css", "./test/stubs/template-passthrough/_site/test.js", ]; for (let path of output) { t.true(fs.existsSync(path)); } deleteDirectory("./test/stubs/template-passthrough/_site/"); }); ================================================ FILE: test/TestUtilityTest.js ================================================ import test from "ava"; import { normalizeNewLines } from "./Util/normalizeNewLines.js"; test("normalizeNewLines", (t) => { t.is(normalizeNewLines("\n"), "\n"); t.is(normalizeNewLines("\r\n"), "\n"); t.is(normalizeNewLines("\r\n\n"), "\n\n"); t.is(normalizeNewLines("\r\n\r\n"), "\n\n"); t.is(normalizeNewLines("a\r\nhello\r\nhi"), "a\nhello\nhi"); }); ================================================ FILE: test/TransformsUtilTest.js ================================================ import test from "ava"; import TransformsUtil from "../src/Util/TransformsUtil.js"; test("TransformsUtil.runall", async (t) => { t.is(await TransformsUtil.runAll("Test content", {}, { test: function(content) { return content + "Overridden!" } }), "Test contentOverridden!"); }); test("TransformsUtil.runall empty warning", async (t) => { t.plan(2); t.is(await TransformsUtil.runAll("Test content", { inputPath: "fake input path", outputPath: "fake output path", }, { test: function() { return ""; } }, { logger: { warn: (message) => { t.is(message, 'Warning: Transform `test` returned empty when writing fake output path from fake input path.'); } } }), ""); }); ================================================ FILE: test/UrlTest.js ================================================ import test from "ava"; import TemplateConfig from "../src/TemplateConfig.js"; import url from "../src/Filters/Url.js"; test("Test url filter passing in pathPrefix from config", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let pp = eleventyConfig.getConfig().pathPrefix; t.is(pp, "/"); t.is(url("test", pp), "test"); t.is(url("/test", pp), "/test"); }); test("Test url filter without passing in pathPrefix", async (t) => { let eleventyConfig = new TemplateConfig(); await eleventyConfig.init(); let urlFilter = eleventyConfig.userConfig.getFilter("url"); t.is(urlFilter("test"), "test"); t.is(urlFilter("/test"), "/test"); }); test("Test url filter with passthrough urls", (t) => { // via https://gist.github.com/mxpv/034933deeebb26b62f14 t.is(url("http://foo.com/blah_blah", ""), "http://foo.com/blah_blah"); t.is(url("http://foo.com/blah_blah/", ""), "http://foo.com/blah_blah/"); t.is(url("http://foo.com/blah_blah_(wikipedia)", ""), "http://foo.com/blah_blah_(wikipedia)"); t.is( url("http://foo.com/blah_blah_(wikipedia)_(again)", ""), "http://foo.com/blah_blah_(wikipedia)_(again)" ); t.is(url("http://www.example.com/wpstyle/?p=364", ""), "http://www.example.com/wpstyle/?p=364"); t.is( url("https://www.example.com/foo/?bar=baz&inga=42&quux", ""), "https://www.example.com/foo/?bar=baz&inga=42&quux" ); t.is( url("http://userid:password@example.com:8080", ""), "http://userid:password@example.com:8080" ); t.is( url("http://userid:password@example.com:8080/", ""), "http://userid:password@example.com:8080/" ); t.is(url("http://userid@example.com", ""), "http://userid@example.com"); t.is(url("http://userid@example.com/", ""), "http://userid@example.com/"); t.is(url("http://userid@example.com:8080", ""), "http://userid@example.com:8080"); t.is(url("http://userid@example.com:8080/", ""), "http://userid@example.com:8080/"); t.is(url("http://userid:password@example.com", ""), "http://userid:password@example.com"); t.is(url("http://userid:password@example.com/", ""), "http://userid:password@example.com/"); t.is(url("http://142.42.1.1/", ""), "http://142.42.1.1/"); t.is(url("http://142.42.1.1:8080/", ""), "http://142.42.1.1:8080/"); t.is(url("http://foo.com/blah_(wikipedia)#cite-1", ""), "http://foo.com/blah_(wikipedia)#cite-1"); t.is( url("http://foo.com/blah_(wikipedia)_blah#cite-1", ""), "http://foo.com/blah_(wikipedia)_blah#cite-1" ); t.is( url("http://foo.com/(something)?after=parens", ""), "http://foo.com/(something)?after=parens" ); t.is( url("http://code.google.com/events/#&product=browser", ""), "http://code.google.com/events/#&product=browser" ); t.is(url("http://j.mp", ""), "http://j.mp"); t.is(url("ftp://foo.bar/baz", ""), "ftp://foo.bar/baz"); t.is( url("http://foo.bar/?q=Test%20URL-encoded%20stuff", ""), "http://foo.bar/?q=Test%20URL-encoded%20stuff" ); t.is( url("http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", ""), "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com" ); t.is(url("http://1337.net", ""), "http://1337.net"); t.is(url("http://a.b-c.de", ""), "http://a.b-c.de"); t.is(url("http://223.255.255.254", ""), "http://223.255.255.254"); t.is(url("http://✪df.ws/123", ""), "http://✪df.ws/123"); t.is(url("http://➡.ws/䨹", ""), "http://➡.ws/䨹"); t.is(url("http://⌘.ws", ""), "http://⌘.ws"); t.is(url("http://⌘.ws/", ""), "http://⌘.ws/"); t.is(url("http://foo.com/unicode_(✪)_in_parens", ""), "http://foo.com/unicode_(✪)_in_parens"); t.is(url("http://☺.damowmow.com/", ""), "http://☺.damowmow.com/"); t.is(url("http://مثال.إختبار", ""), "http://مثال.إختبار"); t.is(url("http://例子.测试", ""), "http://例子.测试"); t.is(url("http://उदाहरण.परीक्षा", ""), "http://उदाहरण.परीक्षा"); }); test("Test url filter", (t) => { t.is(url("/", "/"), "/"); t.is(url("//", "/"), "/"); t.is(url(undefined, "/"), "."); t.is(url("", "/"), "."); // leave . and .. alone t.is(url(".", "/"), "."); t.is(url("./", "/"), "./"); t.is(url("..", "/"), ".."); t.is(url("../", "/"), "../"); t.is(url("test", "/"), "test"); t.is(url("/test", "/"), "/test"); t.is(url("//test", "/"), "//test"); t.is(url("./test", "/"), "test"); t.is(url("../test", "/"), "../test"); t.is(url("test/", "/"), "test/"); t.is(url("/test/", "/"), "/test/"); t.is(url("//test/", "/"), "//test/"); t.is(url("./test/", "/"), "test/"); t.is(url("../test/", "/"), "../test/"); }); test("Test url filter with custom pathPrefix (empty, gets overwritten by root config `/`)", (t) => { t.is(url("/", ""), "/"); t.is(url("//", ""), "/"); t.is(url(undefined, ""), "."); t.is(url("", ""), "."); // leave . and .. alone t.is(url(".", ""), "."); t.is(url("./", ""), "./"); t.is(url("..", ""), ".."); t.is(url("../", ""), "../"); t.is(url("test", ""), "test"); t.is(url("/test", ""), "/test"); t.is(url("//test", ""), "//test"); t.is(url("./test", ""), "test"); t.is(url("../test", ""), "../test"); t.is(url("test/", ""), "test/"); t.is(url("/test/", ""), "/test/"); t.is(url("//test/", ""), "//test/"); t.is(url("./test/", ""), "test/"); t.is(url("../test/", ""), "../test/"); }); test("Test url filter with custom pathPrefix (leading slash)", (t) => { t.is(url("/", "/testdir"), "/testdir/"); t.is(url("//", "/testdir"), "/testdir/"); t.is(url(undefined, "/testdir"), "."); t.is(url("", "/testdir"), "."); // leave . and .. alone t.is(url(".", "/testdir"), "."); t.is(url("./", "/testdir"), "./"); t.is(url("..", "/testdir"), ".."); t.is(url("../", "/testdir"), "../"); t.is(url("test", "/testdir"), "test"); t.is(url("/test", "/testdir"), "/testdir/test"); t.is(url("//test", "/testdir"), "//test"); t.is(url("./test", "/testdir"), "test"); t.is(url("../test", "/testdir"), "../test"); t.is(url("test/", "/testdir"), "test/"); t.is(url("/test/", "/testdir"), "/testdir/test/"); t.is(url("//test/", "/testdir"), "//test/"); t.is(url("./test/", "/testdir"), "test/"); t.is(url("../test/", "/testdir"), "../test/"); }); test("Test url filter with custom pathPrefix (double slash)", (t) => { t.is(url("/", "/testdir/"), "/testdir/"); t.is(url("//", "/testdir/"), "/testdir/"); t.is(url(undefined, "/testdir/"), "."); t.is(url("", "/testdir/"), "."); // leave . and .. alone t.is(url(".", "/testdir/"), "."); t.is(url("./", "/testdir/"), "./"); t.is(url("..", "/testdir/"), ".."); t.is(url("../", "/testdir/"), "../"); t.is(url("test", "/testdir/"), "test"); t.is(url("/test", "/testdir/"), "/testdir/test"); t.is(url("//test", "/testdir/"), "//test"); t.is(url("./test", "/testdir/"), "test"); t.is(url("../test", "/testdir/"), "../test"); t.is(url("test/", "/testdir/"), "test/"); t.is(url("/test/", "/testdir/"), "/testdir/test/"); t.is(url("//test/", "/testdir/"), "//test/"); t.is(url("./test/", "/testdir/"), "test/"); t.is(url("../test/", "/testdir/"), "../test/"); }); test("Test url filter with custom pathPrefix (trailing slash)", (t) => { t.is(url("/", "testdir/"), "/testdir/"); t.is(url("//", "testdir/"), "/testdir/"); t.is(url(undefined, "testdir/"), "."); t.is(url("", "testdir/"), "."); // leave . and .. alone t.is(url(".", "testdir/"), "."); t.is(url("./", "testdir/"), "./"); t.is(url("..", "testdir/"), ".."); t.is(url("../", "testdir/"), "../"); t.is(url("test", "testdir/"), "test"); t.is(url("/test", "testdir/"), "/testdir/test"); t.is(url("//test", "testdir/"), "//test"); t.is(url("./test", "testdir/"), "test"); t.is(url("../test", "testdir/"), "../test"); t.is(url("test/", "testdir/"), "test/"); t.is(url("/test/", "testdir/"), "/testdir/test/"); t.is(url("//test/", "testdir/"), "//test/"); t.is(url("./test/", "testdir/"), "test/"); t.is(url("../test/", "testdir/"), "../test/"); }); test("Test url filter with custom pathPrefix (no slash)", (t) => { t.is(url("/", "testdir"), "/testdir/"); t.is(url("//", "testdir"), "/testdir/"); t.is(url(undefined, "testdir"), "."); t.is(url("", "testdir"), "."); // leave . and .. alone t.is(url(".", "testdir"), "."); t.is(url("./", "testdir"), "./"); t.is(url("..", "testdir"), ".."); t.is(url("../", "testdir"), "../"); t.is(url("test", "testdir"), "test"); t.is(url("/test", "testdir"), "/testdir/test"); t.is(url("//test", "testdir"), "//test"); t.is(url("//foo.com", "testdir"), "//foo.com"); t.is(url("./test", "testdir"), "test"); t.is(url("../test", "testdir"), "../test"); t.is(url("test/", "testdir"), "test/"); t.is(url("/test/", "testdir"), "/testdir/test/"); t.is(url("//test/", "testdir"), "//test/"); t.is(url("./test/", "testdir"), "test/"); t.is(url("../test/", "testdir"), "../test/"); }); ================================================ FILE: test/UserConfigTest.js ================================================ import test from "ava"; import UserConfig from "../src/UserConfig.js"; import memoize from "../src/Util/MemoizeFunction.js"; test("Template Formats", (t) => { let userCfg = new UserConfig(); t.falsy(userCfg.templateFormats); userCfg.setTemplateFormats("njk,liquid"); t.deepEqual(userCfg.templateFormats, "njk,liquid"); // setting multiple times takes the last one userCfg.setTemplateFormats("njk,liquid,pug"); userCfg.setTemplateFormats("njk,liquid"); t.deepEqual(userCfg.templateFormats, "njk,liquid"); }); test("Template Formats (Arrays)", (t) => { let userCfg = new UserConfig(); t.falsy(userCfg.templateFormats); userCfg.setTemplateFormats(["njk", "liquid"]); t.deepEqual(userCfg.templateFormats, ["njk", "liquid"]); // setting multiple times takes the last one userCfg.setTemplateFormats(["njk", "liquid", "pug"]); userCfg.setTemplateFormats(["njk", "liquid"]); t.deepEqual(userCfg.templateFormats, ["njk", "liquid"]); }); // more in TemplateConfigTest.js test("Events", async (t) => { await new Promise((resolve) => { let userCfg = new UserConfig(); userCfg.on("testEvent", function (arg1, arg2, arg3) { t.is(arg1, "arg1"); t.is(arg2, "arg2"); t.is(arg3, "arg3"); resolve(); }); userCfg.emit("testEvent", "arg1", "arg2", "arg3"); }); }); test("Async Events", async (t) => { await new Promise((resolve) => { let userCfg = new UserConfig(); let arg1; userCfg.on( "asyncTestEvent", (_arg1) => new Promise((resolve) => { setTimeout(() => { arg1 = _arg1; resolve(); }, 10); }) ); userCfg.emit("asyncTestEvent", "arg1").then(() => { t.is(arg1, "arg1"); resolve(); }); }); }); test("Add Collections", (t) => { let userCfg = new UserConfig(); userCfg.addCollection("myCollection", function (collection) {}); t.deepEqual(Object.keys(userCfg.getCollections()), ["myCollection"]); }); test("Add Collections throws error on key collision", (t) => { let userCfg = new UserConfig(); userCfg.addCollection("myCollectionCollision", function (collection) {}); t.throws(() => { userCfg.addCollection("myCollectionCollision", function (collection) {}); }); }); test("Set manual Pass-through File Copy (single call)", (t) => { let userCfg = new UserConfig(); userCfg.addPassthroughCopy("img"); t.deepEqual(userCfg.passthroughCopies["img"], { outputPath: true, copyOptions: {}, }); }); test("Set manual Pass-through File Copy (chained calls)", (t) => { let userCfg = new UserConfig(); userCfg .addPassthroughCopy("css") .addPassthroughCopy("js") .addPassthroughCopy({ "./src/static": "static" }) .addPassthroughCopy({ "./src/empty": "./" }); t.deepEqual(userCfg.passthroughCopies["css"], { outputPath: true, copyOptions: {}, }); t.deepEqual(userCfg.passthroughCopies["js"], { outputPath: true, copyOptions: {}, }); t.deepEqual(userCfg.passthroughCopies["./src/static"], { outputPath: "static", copyOptions: {}, }); t.deepEqual(userCfg.passthroughCopies["./src/empty"], { outputPath: "./", copyOptions: {}, }); }); test("Set manual Pass-through File Copy (glob patterns)", (t) => { let userCfg = new UserConfig(); userCfg.addPassthroughCopy({ "./src/static/**/*": "renamed", "./src/markdown/*.md": "", }); // does not exist t.is(userCfg.passthroughCopies["css/**"], undefined); t.is(userCfg.passthroughCopies["js/**"], undefined); // exists t.deepEqual(userCfg.passthroughCopies["./src/static/**/*"], { outputPath: "renamed", copyOptions: {}, }); t.deepEqual(userCfg.passthroughCopies["./src/markdown/*.md"], { outputPath: "", copyOptions: {}, }); }); test("Set Template Formats (string)", (t) => { let userCfg = new UserConfig(); userCfg.setTemplateFormats("njk, liquid"); t.deepEqual(userCfg.templateFormats, "njk, liquid"); }); test("Set Template Formats (array)", (t) => { let userCfg = new UserConfig(); userCfg.setTemplateFormats(["njk", "liquid"]); t.deepEqual(userCfg.templateFormats, ["njk", "liquid"]); }); test("Set Template Formats (js passthrough copy)", (t) => { let userCfg = new UserConfig(); userCfg.setTemplateFormats("njk, liquid, js"); t.deepEqual(userCfg.templateFormats, "njk, liquid, js"); }); test("Set Template Formats (11ty.js)", (t) => { let userCfg = new UserConfig(); userCfg.setTemplateFormats("njk, liquid, 11ty.js"); t.deepEqual(userCfg.templateFormats, "njk, liquid, 11ty.js"); }); test("Add Template Formats", (t) => { let userCfg = new UserConfig(); userCfg.addTemplateFormats("njk"); userCfg.addTemplateFormats("webc"); userCfg.addTemplateFormats("liquid"); userCfg.addTemplateFormats("11ty.js"); t.deepEqual(userCfg.templateFormatsAdded.sort(), ["11ty.js", "liquid", "njk", "webc"]); }); test("Resolve plugin", async (t) => { let userConfig = new UserConfig(); let plugin = await userConfig.resolvePlugin("@11ty/eleventy/html-base-plugin"); t.truthy(typeof plugin === "function") }); test("Resolve plugin (invalid)", async (t) => { let userConfig = new UserConfig(); let e = await t.throwsAsync(async() => { await userConfig.resolvePlugin("@11ty/eleventy/does-not-exist"); }); t.truthy(e.message.startsWith(`Invalid name "@11ty/eleventy/does-not-exist" passed to resolvePlugin.`)); }); test("Memoize filters (control)", (t) => { let userCfg = new UserConfig(); let count = 0; userCfg.addFilter("increment", (num) => { count += num; }); let increment = userCfg.getFilter("increment"); increment(3); t.is(count, 3); increment(3); t.is(count, 6); }); test("Memoize filters (memoized)", (t) => { let userCfg = new UserConfig(); let count = 0; userCfg.addFilter("increment", memoize((num) => { count += num; })); let increment = userCfg.getFilter("increment"); increment(1); increment(1); increment(1); t.is(count, 1); increment(2); increment(2); increment(2); t.is(count, 3); increment(3); increment(3); increment(3); increment(3); t.is(count, 6); }); test("Memoize async filters (memoized)", async (t) => { let userCfg = new UserConfig(); let count = 0; userCfg.addFilter("increment", memoize(async (num) => { return new Promise(resolve => { setTimeout(() => { count += num; resolve(count); }, 50); }); })); let increment = userCfg.getFilter("increment"); await increment(1); await increment(1); await increment(1); t.is(count, 1); await increment(2); await increment(2); await increment(2); t.is(count, 3); await increment(3); await increment(3); await increment(3); await increment(3); t.is(count, 6); }); // JavaScript functions are included here for backwards compatibility https://github.com/11ty/eleventy/issues/3365 test("addJavaScriptFunction feeds into `getFilter` #3365", (t) => { let userCfg = new UserConfig(); userCfg.addJavaScriptFunction("increment", num => num++); t.is(typeof userCfg.getFilter("increment"), "function"); }); ================================================ FILE: test/UserDataExtensionsTest.js ================================================ import test from "ava"; import fs from "fs"; import yaml from "js-yaml"; import TemplateConfig from "../src/TemplateConfig.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import TemplateData from "../src/Data/TemplateData.js"; import { getTemplateConfigInstanceCustomCallback } from "./_testHelpers.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; test("Local data", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-630" }, function(cfg) { cfg.addDataExtension("yaml", { parser: (s) => yaml.load(s) }); cfg.addDataExtension("nosj", { parser: (s) => JSON.parse(s) }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); // YAML GLOBAL DATA t.is(data.globalData3.datakey1, "datavalue3"); t.is(data.globalData3.datakey2, "{{pkg.name}}--yaml"); // NOSJ (JSON) GLOBAL DATA t.is(data.globalData4.datakey1, "datavalue4"); t.is(data.globalData4.datakey2, "{{pkg.name}}--nosj"); let withLocalData = await dataObj.getTemplateDirectoryData( "./test/stubs-630/component-yaml/component.njk" ); t.is(withLocalData.yamlKey1, "yaml1"); t.is(withLocalData.yamlKey2, "yaml2"); t.is(withLocalData.yamlKey3, "yaml3"); t.is(withLocalData.nosjKey1, "nosj1"); t.is(withLocalData.jsonKey1, "json1"); t.is(withLocalData.jsonKey2, "json2"); t.is(withLocalData.jsKey1, "js1"); }); test("Local files", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-630" }, function(cfg) { cfg.addDataExtension("yaml", { parser: (s) => yaml.load(s) }); cfg.addDataExtension("nosj", { parser: (s) => JSON.parse(s) }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); let files = await dataObj.getLocalDataPaths("./test/stubs-630/component-yaml/component.njk"); t.deepEqual(files, [ "./test/stubs-630/stubs-630.yaml", "./test/stubs-630/stubs-630.nosj", "./test/stubs-630/stubs-630.json", "./test/stubs-630/stubs-630.11tydata.yaml", "./test/stubs-630/stubs-630.11tydata.nosj", "./test/stubs-630/stubs-630.11tydata.json", "./test/stubs-630/stubs-630.11tydata.mjs", "./test/stubs-630/stubs-630.11tydata.cjs", "./test/stubs-630/stubs-630.11tydata.js", "./test/stubs-630/component-yaml/component-yaml.yaml", "./test/stubs-630/component-yaml/component-yaml.nosj", "./test/stubs-630/component-yaml/component-yaml.json", "./test/stubs-630/component-yaml/component-yaml.11tydata.yaml", "./test/stubs-630/component-yaml/component-yaml.11tydata.nosj", "./test/stubs-630/component-yaml/component-yaml.11tydata.json", "./test/stubs-630/component-yaml/component-yaml.11tydata.mjs", "./test/stubs-630/component-yaml/component-yaml.11tydata.cjs", "./test/stubs-630/component-yaml/component-yaml.11tydata.js", "./test/stubs-630/component-yaml/component.yaml", "./test/stubs-630/component-yaml/component.nosj", "./test/stubs-630/component-yaml/component.json", "./test/stubs-630/component-yaml/component.11tydata.yaml", "./test/stubs-630/component-yaml/component.11tydata.nosj", "./test/stubs-630/component-yaml/component.11tydata.json", "./test/stubs-630/component-yaml/component.11tydata.mjs", "./test/stubs-630/component-yaml/component.11tydata.cjs", "./test/stubs-630/component-yaml/component.11tydata.js", ]); }); test("Global data", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-630" }, function(cfg) { cfg.addDataExtension("yaml", { parser: (s) => yaml.load(s) }); cfg.addDataExtension("nosj", { parser: (s) => JSON.parse(s) }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); t.deepEqual(dataObj.getGlobalDataGlob(), [ "./test/stubs-630/_data/**/*.{nosj,yaml,json,mjs,cjs,js}", ]); let data = await dataObj.getGlobalData(); // JS GLOBAL DATA t.is(data.globalData0.datakey1, "datavalue0"); // CJS GLOBAL DATA t.is(data.globalData1.datakey1, "datavalue1"); // JSON GLOBAL DATA t.is(data.globalData2.datakey1, "datavalue2"); t.is(data.globalData2.datakey2, "{{pkg.name}}--json"); // YAML GLOBAL DATA t.is(data.globalData3.datakey1, "datavalue3"); t.is(data.globalData3.datakey2, "{{pkg.name}}--yaml"); // NOSJ (JSON) GLOBAL DATA t.is(data.globalData4.datakey1, "datavalue4"); t.is(data.globalData4.datakey2, "{{pkg.name}}--nosj"); t.is(data.subdir.globalDataSubdir.keyyaml, "yaml"); }); test("Global data merging and priority", async (t) => { let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-630" }, function(cfg) { cfg.addDataExtension("yaml", { parser: (s) => yaml.load(s) }); cfg.addDataExtension("nosj", { parser: (s) => JSON.parse(s) }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); // TESTING GLOBAL DATA PRIORITY AND MERGING t.is(data.mergingGlobalData.datakey0, "js-value0"); t.is(data.mergingGlobalData.datakey1, "cjs-value1"); t.is(data.mergingGlobalData.datakey2, "json-value2"); t.is(data.mergingGlobalData.datakey3, "yaml-value3"); t.is(data.mergingGlobalData.datakey4, "nosj-value4"); t.is(data.mergingGlobalData.jskey, "js"); t.is(data.mergingGlobalData.cjskey, "cjs"); t.is(data.mergingGlobalData.jsonkey, "json"); t.is(data.mergingGlobalData.yamlkey, "yaml"); t.is(data.mergingGlobalData.nosjkey, "nosj"); }); test("Binary data files, encoding: null", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-2378" }, function(cfg) { cfg.addDataExtension("jpg", { parser: (s) => { t.true(Buffer.isBuffer(s)); // s is a Buffer, just return the length as a sample return s.length; }, encoding: null, }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.images.dog, 43183); }); test("Binary data files, read: false", async (t) => { t.plan(2); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-2378" }, function(cfg) { cfg.addDataExtension("jpg", { parser: (s) => { t.true(fs.existsSync(s)); // s is a Buffer, just return the length as a sample return s; }, read: false, }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.images.dog, "./test/stubs-2378/_data/images/dog.jpg"); }); test("Binary data files, encoding: null (multiple data extensions)", async (t) => { t.plan(4); let eleventyConfig = await getTemplateConfigInstanceCustomCallback( { input: "test/stubs-2378" }, function(cfg) { cfg.addDataExtension("jpg, png", { parser: function (s) { t.true(Buffer.isBuffer(s)); // s is a Buffer, just return the length as a sample return s.length; }, encoding: null, }); } ); let dataObj = new TemplateData(eleventyConfig); dataObj.extensionMap = new EleventyExtensionMap(eleventyConfig); dataObj.setProjectUsingEsm(true); dataObj.setFileSystemSearch(new FileSystemSearch()); let data = await dataObj.getGlobalData(); t.is(data.images.dog, 43183); t.is(data.images.dogpng, 2890); }); test("Missing `parser` property to addDataExtension object throws error", async (t) => { let eleventyConfig = new TemplateConfig(); t.throws(() => { eleventyConfig.userConfig.addDataExtension("jpg", {}); }, { message: "Expected `parser` property in second argument object to `eleventyConfig.addDataExtension`" }); }); ================================================ FILE: test/Util/normalizeNewLines.js ================================================ import os from 'node:os'; function normalizeNewLines(str) { return str.replace(/\r\n/g, "\n"); } function localizeNewLines(str) { return normalizeNewLines(str).replace(/\n/g, os.EOL); } export { normalizeNewLines, localizeNewLines, }; ================================================ FILE: test/Util/normalizeSeparators.js ================================================ import PathNormalizer from "../../src/Util/PathNormalizer.js"; export function normalizeSeparatorString(str) { return PathNormalizer.normalizeSeperator(str); } export function normalizeSeparatorArray(arr) { return arr.map(entry => { return PathNormalizer.normalizeSeperator(entry); }) } ================================================ FILE: test/UtilSetUnionTest.js ================================================ import test from "ava"; import { union } from "../src/Util/SetUtil.js"; test("Basic set union (zero)", t => { t.deepEqual(union(), new Set()); }); test("Basic set union (one)", t => { let a = new Set([1,2,3]); t.deepEqual(union(a), new Set([1,2,3])); }); test("Basic set union (two)", t => { let a = new Set([1,2,3]); let b = new Set([3,4,5]); t.deepEqual(union(a, b), new Set([1,2,3,4,5])); }); test("Basic set union (three)", t => { let a = new Set([0,1,2,3]); let b = new Set([3,4,5]); let c = new Set([3,4,5,6]); t.deepEqual(union(a, b, c), new Set([0,1,2,3,4,5,6])); }); ================================================ FILE: test/WatchQueueTest.js ================================================ import test from "ava"; import WatchQueue from "../src/WatchQueue.js"; test("Standard", (t) => { let watch = new WatchQueue(); t.is(watch.isBuildRunning(), false); watch.setBuildRunning(); t.is(watch.isBuildRunning(), true); watch.setBuildFinished(); t.is(watch.isBuildRunning(), false); }); test("Incremental", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); watch.incremental = true; t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.addToPendingQueue("test.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), "./test.md"); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); t.deepEqual(watch.getPendingQueue(), []); }); test("Incremental queue 2", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); watch.incremental = true; t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.addToPendingQueue("test.md"); watch.addToPendingQueue("test2.md"); t.is(watch.getPendingQueueSize(), 2); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), "./test.md"); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getPendingQueue(), ["./test2.md"]); t.deepEqual(watch.getActiveQueue(), []); }); test("Incremental add while active", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); watch.incremental = true; t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.addToPendingQueue("test.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), "./test.md"); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.addToPendingQueue("test2.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), "./test.md"); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getPendingQueue(), ["./test2.md"]); t.deepEqual(watch.getActiveQueue(), []); }); test("Non-incremental", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.addToPendingQueue("test.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); }); test("Non-incremental queue 2", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.addToPendingQueue("test.md"); watch.addToPendingQueue("test2.md"); t.is(watch.getPendingQueueSize(), 2); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 2); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), ["./test.md", "./test2.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getPendingQueue(), []); t.deepEqual(watch.getActiveQueue(), []); }); test("Non-incremental add while active", (t) => { let watch = new WatchQueue(); t.is(watch.getIncrementalFile(), false); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getIncrementalFile(), false); watch.addToPendingQueue("test.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), []); watch.setBuildRunning(); t.is(watch.getPendingQueueSize(), 0); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.addToPendingQueue("test.md"); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 1); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getActiveQueue(), ["./test.md"]); watch.setBuildFinished(); t.is(watch.getPendingQueueSize(), 1); t.is(watch.getActiveQueueSize(), 0); t.is(watch.getIncrementalFile(), false); t.deepEqual(watch.getPendingQueue(), ["./test.md"]); t.deepEqual(watch.getActiveQueue(), []); }); test("Active queue tests", (t) => { let watch = new WatchQueue(); watch.addToPendingQueue("test.md"); watch.addToPendingQueue("test2.md"); watch.addToPendingQueue("test.css"); t.is( watch.hasAllQueueFiles((path) => path.startsWith("./test")), false ); watch.setBuildRunning(); t.is(watch.hasAllQueueFiles("slkdjflkjsdlkfj"), false); t.is( watch.hasAllQueueFiles((path) => path.startsWith("./test")), true ); t.is( watch.hasAllQueueFiles((path) => path.endsWith(".css")), false ); t.is(watch.hasQueuedFile("./test.md"), true); t.is(watch.hasQueuedFile("./testsdkljfklja.md"), false); watch.setBuildFinished(); t.is( watch.hasAllQueueFiles((path) => path.startsWith("./test")), false ); }); test("Active queue tests, all CSS files", (t) => { let watch = new WatchQueue(); watch.addToPendingQueue("test.css"); watch.addToPendingQueue("test2.css"); watch.addToPendingQueue("test3.css"); t.is( watch.hasAllQueueFiles((path) => path.endsWith(".css")), false ); watch.setBuildRunning(); t.is( watch.hasAllQueueFiles((path) => path.endsWith(".css")), true ); watch.setBuildFinished(); t.is( watch.hasAllQueueFiles((path) => path.endsWith(".css")), false ); }); ================================================ FILE: test/WatchTargetsTest.js ================================================ import test from "ava"; import TemplateConfig from "../src/TemplateConfig.js"; import EleventyWatchTargets from "../src/WatchTargets.js"; import JavaScriptDependencies from "../src/Util/JavaScriptDependencies.js"; test("Basic", (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); t.deepEqual(targets.getTargets(), []); targets.add(".eleventy.js"); t.deepEqual(targets.getTargets(), ["./.eleventy.js"]); }); test("Removes duplicates", (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); targets.add(".eleventy.js"); targets.add("./.eleventy.js"); t.deepEqual(targets.getTargets(), ["./.eleventy.js"]); }); test("Add array", (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); targets.add([".eleventy.js", "b.js"]); targets.add(["b.js", "c.js"]); t.deepEqual(targets.getTargets(), ["./.eleventy.js", "./b.js", "./c.js"]); }); test("Add and make glob", (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); // Note the `test` directory must exist here for this to pass. targets.add(["test", "test/b.js"]); t.deepEqual(targets.getTargets(), ["./test", "./test/b.js"]); }); test("JavaScript get dependencies", async (t) => { t.deepEqual( await JavaScriptDependencies.getDependencies(["./test/stubs/config-deps.cjs"], true), ["./test/stubs/config-deps-upstream.cjs"] ); }); test("JavaScript addDependencies", async (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); await targets.addDependencies("./test/stubs/config-deps.cjs"); t.deepEqual(targets.getTargets(), ["./test/stubs/config-deps-upstream.cjs"]); t.true(targets.uses("./test/stubs/config-deps.cjs", "./test/stubs/config-deps-upstream.cjs")); t.false(targets.uses("./test/stubs/config-deps.cjs", "./test/stubs/config-deps.cjs")); }); test("JavaScript addDependencies (one file has two dependencies)", async (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); await targets.addDependencies("./test/stubs/dependencies/two-deps.11ty.cjs"); t.deepEqual(targets.getTargets(), [ "./test/stubs/dependencies/dep1.cjs", "./test/stubs/dependencies/dep2.cjs", ]); t.true( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep1.cjs" ) ); t.true( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep2.cjs" ) ); t.false( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep3.cjs" ) ); }); test("JavaScript addDependencies (skip JS deps)", async (t) => { let templateConfig = new TemplateConfig(); let targets = new EleventyWatchTargets(templateConfig); targets.setProjectUsingEsm(true); targets.watchJavaScriptDependencies = false; await targets.addDependencies("./test/stubs/dependencies/two-deps.11ty.cjs"); t.deepEqual(targets.getTargets(), []); t.false( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep1.cjs" ) ); t.false( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep2.cjs" ) ); t.false( targets.uses( "./test/stubs/dependencies/two-deps.11ty.cjs", "./test/stubs/dependencies/dep3.cjs" ) ); }); test("JavaScript addDependencies with a filter", async (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); await targets.addDependencies("./test/stubs/config-deps.cjs", function (path) { return path.indexOf("./test/stubs/") === -1; }); t.deepEqual(targets.getTargets(), []); t.false( targets.uses( "./test/stubs/dependencies/config-deps.cjs", "./test/stubs/dependencies/config-deps-upstream.cjs" ) ); }); test("add, addDependencies falsy values are filtered", async (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); targets.add(""); await targets.addDependencies(""); t.deepEqual(targets.getTargets(), []); }); test("add, addDependencies file does not exist", async (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); targets.add("./.eleventy-notfound.js"); // does not exist await targets.addDependencies("./.eleventy-notfound.js"); // does not exist t.deepEqual(targets.getTargets(), ["./.eleventy-notfound.js"]); }); test("getNewTargetsSinceLastReset", (t) => { let targets = new EleventyWatchTargets(); targets.setProjectUsingEsm(true); targets.add("./.eleventy-notfound.js"); // does not exist t.deepEqual(targets.getNewTargetsSinceLastReset(), ["./.eleventy-notfound.js"]); t.deepEqual(targets.getNewTargetsSinceLastReset(), ["./.eleventy-notfound.js"]); targets.reset(); targets.add("./.eleventy-notfound2.js"); t.deepEqual(targets.getNewTargetsSinceLastReset(), ["./.eleventy-notfound2.js"]); targets.reset(); t.deepEqual(targets.getNewTargetsSinceLastReset(), []); }); ================================================ FILE: test/_getNewTemplateForTests.js ================================================ import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import Template from "../src/Template.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; export default async function getNewTemplate( path, inputDir, outputDir, templateData = null, map = null, eleventyConfig = null ) { if (!eleventyConfig) { eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir, output: outputDir, } }); } let engineManager = new TemplateEngineManager(eleventyConfig); if (!map) { map = new EleventyExtensionMap(eleventyConfig); map.setFormats(["liquid", "md", "njk", "html", "11ty.js"]); map.engineManager = engineManager; } if (templateData) { templateData.setFileSystemSearch(new FileSystemSearch()); templateData.extensionMap = map; } let tmpl = new Template(path, templateData, map, eleventyConfig); await tmpl.getTemplateRender(); return tmpl; } ================================================ FILE: test/_getRenderedTemplates.js ================================================ async function getRenderedTemplates(template, data) { let pages = await template.getTemplates(data); await Promise.all( pages.map(async (page) => { let content = await renderTemplate(page.template, page.data); page.templateContent = content; }) ); return pages; } async function renderLayout(tmpl, tmplData) { let content = await tmpl.renderPageEntryWithoutLayout({ rawInput: await tmpl.getPreRender(), data: tmplData }); let layoutKey = tmplData[tmpl.config.keys.layout]; let layout = tmpl.getLayout(layoutKey); return layout.renderPageEntry({ data: tmplData, templateContent: content, }); } async function renderLayoutViaLayout(layout, tmplData, templateContent) { return layout.renderPageEntry({ data: tmplData, templateContent, }); } async function renderTemplate(tmpl, tmplData) { if (!tmplData) { throw new Error("`tmplData` needs to be passed into render()"); } if (tmplData[tmpl.config.keys.layout]) { return renderLayout(tmpl, tmplData); } else { return tmpl.renderPageEntryWithoutLayout({ rawInput: await tmpl.getPreRender(), data: tmplData }); } } export { getRenderedTemplates, renderLayoutViaLayout, renderLayout, renderTemplate, }; ================================================ FILE: test/_issues/0/content/index.html ================================================

HTML

================================================ FILE: test/_issues/0/eleventy.config.js ================================================ export default function(cfg) { }; export const config = { dir: {} } ================================================ FILE: test/_issues/0/issue-0-test.js ================================================ import test from "ava"; import { fileURLToPath } from "node:url"; import { parse } from "node:path"; import { spawnAsync } from "../../../src/Util/spawn.js"; const CURRENT_DIR = parse(fileURLToPath(import.meta.url)).dir; test.skip("Issue #0 (this is a stub file)", async (t) => { let result = await spawnAsync( "node", ["../../../cmd.cjs", "--to=json"], { cwd: CURRENT_DIR, } ); let json = JSON.parse(result); t.is(json.length, 1); t.is(json[0]?.content.trim(), "

HTML

"); }); ================================================ FILE: test/_issues/2250/2250-test.js ================================================ import test from "ava"; import Eleventy from "../../../src/Eleventy.js"; test("Issue #2250, page is available in filters", async (t) => { let elev = new Eleventy("./test/_issues/2250/", "./test/_issues/2250/_site", { config: function (eleventyConfig) { eleventyConfig.addFilter("getUrl", function () { return this.page.url; }); }, }); let results = await elev.toJSON(); let nunjucks = results.filter((entry) => { return entry.url.startsWith("/nunjucks/"); }); t.is(nunjucks[0].content.trim(), "/nunjucks/"); let liquid = results.filter((entry) => { return entry.url.startsWith("/liquid/"); }); t.is(liquid[0].content.trim(), "/liquid/"); let javascript = results.filter((entry) => { return entry.url.startsWith("/javascript/"); }); t.is(javascript[0].content.trim(), "/javascript/"); }); ================================================ FILE: test/_issues/2250/javascript.11ty.cjs ================================================ module.exports = function () { return this.getUrl(); }; ================================================ FILE: test/_issues/2250/liquid.liquid ================================================ {{ "test" | getUrl }} ================================================ FILE: test/_issues/2250/nunjucks.njk ================================================ {{ "test" | getUrl }} ================================================ FILE: test/_issues/3697/3697-test.js ================================================ // import path from "node:path"; // import { fileURLToPath } from "node:url"; import test from "ava"; import Eleventy from "../../../src/Eleventy.js"; test("Number file names on global data files", async t => { // TODO fix absolute paths here // let dir = path.parse(fileURLToPath(import.meta.url)).dir; let dir = "./test/_issues/3697/"; let elev = new Eleventy(dir, undefined, { config: function (eleventyConfig) { eleventyConfig.addTemplate("index.11ty.js", function(data) { return '' + JSON.stringify(data.folder); }) }, }); let results = await elev.toJSON(); t.is(results.length, 1); t.is(results[0].content, `[{"key":"value"},null,null,{}]`); }); ================================================ FILE: test/_issues/3697/_data/folder/0.json ================================================ { "key": "value" } ================================================ FILE: test/_issues/3697/_data/folder/3.json ================================================ {} ================================================ FILE: test/_issues/3809/.app/.eleventy.js ================================================ export const config = { dir: { input: "../", data: ".app/_data", } }; ================================================ FILE: test/_issues/3809/.app/_data/app.json ================================================ {"name": "My Application"} ================================================ FILE: test/_issues/3809/index.njk ================================================ {{ app.name }} ================================================ FILE: test/_issues/3853/deeper/index.njk ================================================ 3853 ================================================ FILE: test/_issues/3854/app/.eleventy.js ================================================ export const config = { dir: { input: "../", } }; ================================================ FILE: test/_issues/3854/app/index.njk ================================================ 3854/child ================================================ FILE: test/_issues/3854/index.njk ================================================ 3854/parent ================================================ FILE: test/_issues/3896/eleventy-input-folder/3896.html ================================================ Issue 3896 ================================================ FILE: test/_issues/3896/eleventy-input-folder/_archive/ignored.html ================================================ This should be ignored ================================================ FILE: test/_issues/3896/test-files/eleventy.config.js ================================================ import path from "node:path"; export default function(cfg) { // Works // cfg.ignores.add("../**/_archive/**"); cfg.ignores.add("**/_archive/**"); }; export const config = { dir: { input: path.resolve("../eleventy-input-folder"), output: path.resolve("../_site") } } ================================================ FILE: test/_issues/3896/test-files/issue3896-test.js ================================================ import test from "ava"; import { TemplatePath } from "@11ty/eleventy-utils"; import { spawnAsync } from "../../../../src/Util/spawn.js"; test("#3896 ignores should respect relative parent directory ../", async (t) => { let result = await spawnAsync( "node", ["../../../../cmd.cjs", "--to=json"], { cwd: "test/_issues/3896/test-files/" } ); let json = JSON.parse(result); t.is(json.length, 1); t.is(json[0]?.outputPath, TemplatePath.standardizeFilePath("../_site/3896/index.html")); t.is(json[0]?.content.trim(), "Issue 3896"); }); ================================================ FILE: test/_issues/3932/1/2025.html ================================================ {{ page.filePathStem }} ================================================ FILE: test/_issues/3932/eleventy.config.js ================================================ export default function(cfg) { }; export const config = { dir: {} } ================================================ FILE: test/_issues/3932/issue-3932-test.js ================================================ import test from "ava"; import { fileURLToPath } from "node:url"; import { parse } from "node:path"; import { spawnAsync } from "../../../src/Util/spawn.js"; const CURRENT_DIR = parse(fileURLToPath(import.meta.url)).dir; test("Issue #3932", async (t) => { let result = await spawnAsync( "node", ["../../../cmd.cjs", "--to=json"], { cwd: CURRENT_DIR, } ); let json = JSON.parse(result); t.is(json.length, 1); t.is(json[0]?.inputPath.trim(), "./1/2025.html"); t.is(json[0]?.content.trim(), "/1/2025"); t.is(json[0]?.outputPath.trim(), "./_site/1/2025/index.html"); }); ================================================ FILE: test/_issues/975/975-test.js ================================================ import test from "ava"; import TemplateMap from "../../../src/TemplateMap.js"; import getNewTemplateForTests from "../../_getNewTemplateForTests.js"; import { getTemplateConfigInstance } from "../../_testHelpers.js"; function getNewTemplate(filename, input, output, eleventyConfig) { return getNewTemplateForTests(filename, input, output, null, null, eleventyConfig); } test("Get ordered list of templates", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/_issues/975/", output: "test/_issues/975/_site", } }); let tm = new TemplateMap(eleventyConfig); // These two templates are add-order-dependent await tm.add( await getNewTemplate( "./test/_issues/975/post.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); await tm.add( await getNewTemplate( "./test/_issues/975/another-post.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); // This template should be last await tm.add( await getNewTemplate( "./test/_issues/975/index.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); await tm.cache(); let order = tm.getTemplateOrder(); t.deepEqual(order, [ "./test/_issues/975/post.md", "./test/_issues/975/another-post.md", "__collection:post", "__collection:[keys]", "./test/_issues/975/index.md", "__collection:all", ]); }); test("Get ordered list of templates (reverse add)", async (t) => { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: "test/_issues/975/", output: "test/_issues/975/_site", } }); let tm = new TemplateMap(eleventyConfig); // This template is now first await tm.add( await getNewTemplate( "./test/_issues/975/index.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); // These two templates are add-order-dependent await tm.add( await getNewTemplate( "./test/_issues/975/another-post.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); await tm.add( await getNewTemplate( "./test/_issues/975/post.md", "./test/_issues/975/", "./test/_issues/975/_site", eleventyConfig ) ); await tm.cache(); let order = tm.getTemplateOrder(); t.deepEqual(order, [ "./test/_issues/975/another-post.md", "./test/_issues/975/post.md", "__collection:post", "__collection:[keys]", "./test/_issues/975/index.md", "__collection:all", ]); }); ================================================ FILE: test/_issues/975/another-post.md ================================================ --- tags: - post --- ================================================ FILE: test/_issues/975/index.md ================================================ --- eleventyImport: collections: ["post"] --- ================================================ FILE: test/_issues/975/post.md ================================================ --- tags: - post --- ================================================ FILE: test/_testHelpers.js ================================================ import { existsSync, rmSync } from "node:fs"; import { isPlainObject } from "@11ty/eleventy-utils"; import TemplateConfig from "../src/TemplateConfig.js"; import ProjectDirectories from "../src/Util/ProjectDirectories.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import TemplatePassthroughManager from "../src/TemplatePassthroughManager.js"; import EleventyFiles from "../src/EleventyFiles.js"; import FileSystemSearch from "../src/FileSystemSearch.js"; import TemplateWriter from "../src/TemplateWriter.js"; import TemplateEngineManager from "../src/Engines/TemplateEngineManager.js"; import TemplateData from "../src/Data/TemplateData.js"; export async function getTemplateConfigInstance(configObj, dirs, configObjOverride = undefined) { let eleventyConfig; if(configObj instanceof TemplateConfig) { eleventyConfig = configObj; configObj = undefined; if(!(dirs instanceof ProjectDirectories)) { throw new Error("Testing error: second argument to getTemplateConfigInstance must be a ProjectDirectories instance when the first argument is a TemplateConfig instance.") } } else { eleventyConfig = new TemplateConfig(); } eleventyConfig.setProjectUsingEsm(true); if(!(dirs instanceof ProjectDirectories)) { dirs = new ProjectDirectories(); if(isPlainObject(configObj) && !configObj.dir) { throw new Error("Testing error: missing `dir` property on config object literal passed in.") } dirs.setViaConfigObject(configObj?.dir || {}); } eleventyConfig.setDirectories(dirs); await eleventyConfig.init(configObjOverride || configObj); // overrides return eleventyConfig; } export async function getTemplateConfigInstanceCustomCallback(dirObject, configCallback) { let tmplCfg = new TemplateConfig(); configCallback(tmplCfg.userConfig); let dirs = new ProjectDirectories(); dirs.setViaConfigObject(dirObject); let eleventyConfig = await getTemplateConfigInstance(tmplCfg, dirs, { dir: dirObject }); return eleventyConfig; } export function getTemplateWriterInstance(formats, templateConfig) { let { eleventyFiles, passthroughManager } = getEleventyFilesInstance(formats, templateConfig); let templateWriter = new TemplateWriter( formats, null, templateConfig, ); let engineManager = new TemplateEngineManager(templateConfig); let map = new EleventyExtensionMap(templateConfig); map.engineManager = engineManager; map.setFormats(formats); templateWriter.extensionMap = map; templateWriter.setEleventyFiles(eleventyFiles); templateWriter.setPassthroughManager(passthroughManager); return { templateWriter, eleventyFiles, passthroughManager, } } export function getEleventyFilesInstance(formats, templateConfig) { let map = new EleventyExtensionMap(templateConfig); map.setFormats(formats); let fss = new FileSystemSearch(); let mgr = new TemplatePassthroughManager(templateConfig); mgr.extensionMap = map; mgr.setFileSystemSearch(fss); let files = new EleventyFiles(formats, templateConfig); files.setPassthroughManager(mgr); files.setFileSystemSearch(fss); files.extensionMap = map; files.templateData = new TemplateData(templateConfig); files.init(); return { eleventyFiles: files, passthroughManager: mgr, }; } export function sortEleventyResults(a, b) { if(b.inputPath > a.inputPath) { return 1; } else if(b.inputPath < a.inputPath) { return -1; } return 0; } export function deleteDirectory(dir) { if(existsSync(dir)) { rmSync(dir, { recursive: true }); } } ================================================ FILE: test/cmdTest.js ================================================ import test from "ava"; import { exec } from "child_process"; test("Test command line exit code success", async (t) => { await new Promise((resolve) => { exec("node ./cmd.cjs --input=test/stubs/exitCode_success --dryrun", (error, stdout, stderr) => { t.falsy(error); resolve(); }); }); }); test("Test command line exit code for template error", async (t) => { await new Promise((resolve) => { exec("node ./cmd.cjs --input=test/stubs/exitCode --dryrun", (error, stdout, stderr) => { t.is(error.code, 1); resolve(); }); }); }); test("Test command line exit code for global data error", async (t) => { await new Promise((resolve) => { exec( "node ./cmd.cjs --input=test/stubs/exitCode_globalData --dryrun", (error, stdout, stderr) => { t.is(error.code, 1); resolve(); } ); }); }); test("Test data should not process in a --help", async (t) => { await new Promise((resolve) => { exec( "node ./cmd.cjs --input=test/stubs/cmd-help-processing --help", (error, stdout, stderr) => { t.falsy(error); t.is(stdout.indexOf("THIS SHOULD NOT LOG TO CONSOLE"), -1); resolve(); } ); }); }); test("Test data should not process in a --version", async (t) => { await new Promise((resolve) => { exec( "node ./cmd.cjs --input=test/stubs/cmd-help-processing --version", (error, stdout, stderr) => { t.falsy(error); t.is(stdout.indexOf("THIS SHOULD NOT LOG TO CONSOLE"), -1); resolve(); } ); }); }); ================================================ FILE: test/file-system-search/file.txt ================================================ ================================================ FILE: test/noop/.gitkeep ================================================ ================================================ FILE: test/noop2/.gitkeep ================================================ ================================================ FILE: test/proxy-pagination-globaldata/_data/banner.js ================================================ export default { content: "BANNER TEXT", }; ================================================ FILE: test/proxy-pagination-globaldata/tmpl.liquid ================================================ --- pages: - page 1 pagination: data: pages size: 1 --- {{ banner.content }} ================================================ FILE: test/proxy-pagination-globaldata/tmpl2.njk ================================================ --- pages: - page 1 pagination: data: pages size: 1 --- {{ banner.content }} ================================================ FILE: test/proxy-pagination-globaldata/tmpl4.11ty.js ================================================ const data = { pages: ["page 1"], pagination: { data: "pages", size: 1, }, }; const render = (data) => `${data.banner.content}`; export { data, render }; ================================================ FILE: test/semverCoerceTest.js ================================================ import test from "ava"; import { coerce } from "../src/Util/SemverCoerce.js"; test("semverCoerce", t => { t.is(coerce("4.0.0"), "4.0.0"); t.is(coerce("4.0.0-prerelease"), "4.0.0"); t.is(coerce("4.0"), "4.0"); t.is(coerce("v4.0"), "4.0"); }); ================================================ FILE: test/slugify-filter/comma.njk ================================================ --- title: "Hi, I'm ZAch" permalink: subdir/{{ title | slugify }}/index.html --- Slugged. ================================================ FILE: test/slugify-filter/multibyte.njk ================================================ --- title: "test-猫" permalink: subdir/{{ title | slugify }}/index.html --- Slugged. ================================================ FILE: test/slugify-filter/slug-number.njk ================================================ --- number: 1 permalink: subdir/{{ number | slugify }}/index.html --- Slugged. ================================================ FILE: test/slugify-filter/slug-options.njk ================================================ --- title: "Hi, I am ZAch" permalink: "subdir/{{ title | slugify({separator:'_'}) }}/index.html" --- Slugged. ================================================ FILE: test/slugify-filter/slugify-number.njk ================================================ --- number: 1 permalink: subdir/{{ number | slugify }}/index.html --- Slugged. ================================================ FILE: test/slugify-filter/slugify-options.njk ================================================ --- title: "Hi, I'm ZAch" permalink: "subdir/{{ title | slugify({decamelize: true}) }}/index.html" --- Slugged. ================================================ FILE: test/slugify-filter/test.njk ================================================ --- title: _Slug ♥ CANDIDATE люблю $#%- permalink: subdir/{{ title | slugify }}/index.html --- Slugged. ================================================ FILE: test/stubs/.eleventyignore ================================================ ignoredFolder ./ignoredFolder/ignored.md # This is a comment ================================================ FILE: test/stubs/2016-02-01-permalinkdate.liquid ================================================ --- title: Date Permalink permalink: "/{{ page.date | date: '%Y/%m/%d' }}/index.html" --- Date Permalinks ================================================ FILE: test/stubs/_data/globalData.json ================================================ { "datakey1": "datavalue1", "datakey2": "{{pkg.name}}" } ================================================ FILE: test/stubs/_data/globalData2.cjs ================================================ module.exports = { datakeyfromjs: "howdy" }; ================================================ FILE: test/stubs/_data/globalDataFn.js ================================================ import dep1 from "../deps/dep1.cjs"; export default function () { return { datakeyfromjsfn: "howdy", }; } ================================================ FILE: test/stubs/_data/globalDataFnCJS.cjs ================================================ const dep1 = require("../deps/dep1.cjs"); module.exports = function() { return { datakeyfromcjsfn: "common-cjs-howdy" }; }; ================================================ FILE: test/stubs/_data/subdir/testDataSubdir.json ================================================ { "subdirkey": "subdirvalue" } ================================================ FILE: test/stubs/_data/testData.json ================================================ { "testdatakey1": "testdatavalue1" } ================================================ FILE: test/stubs/_data/testDataLiquid.json ================================================ { "datakey1": "datavalue1", "datakey2": "{{ pkg.name }}" } ================================================ FILE: test/stubs/_includes/base.njk ================================================

{% block content %}This is a parent.{% endblock %}

================================================ FILE: test/stubs/_includes/custom-filter.liquid ================================================ {{ name | makeItFoo }} ================================================ FILE: test/stubs/_includes/default.liquid ================================================ ================================================ FILE: test/stubs/_includes/defaultLayout.liquid ================================================ --- keylayout: valuelayout postRank: 4 daysPosted: 152 yearsPosted: 0.4 ---
{{ content }}
================================================ FILE: test/stubs/_includes/defaultLayoutLayoutContent.liquid ================================================ --- keylayout: valuelayout postRank: 4 daysPosted: 152 yearsPosted: 0.4 ---
{{ content }}
================================================ FILE: test/stubs/_includes/imports.njk ================================================ {% macro label(text) %}{% endmacro %} ================================================ FILE: test/stubs/_includes/included-data.html ================================================ This is an include. {{ myVariable }} ================================================ FILE: test/stubs/_includes/included-relative.njk ================================================ akdlsjafkljdskl ================================================ FILE: test/stubs/_includes/included.html ================================================ This is an include. ================================================ FILE: test/stubs/_includes/included.liquid ================================================ This is an include. ================================================ FILE: test/stubs/_includes/included.njk ================================================ This is an include. ================================================ FILE: test/stubs/_includes/included.nunj ================================================ Nunjabusiness ================================================ FILE: test/stubs/_includes/layout-a.liquid ================================================ --- layout: layout-b key1: value1-a upstream: value2-a ---
{{ content }}
================================================ FILE: test/stubs/_includes/layout-b.liquid ================================================ --- key1: value1-b upstream: value2-b daysPosted: 154 ---
{{ content }}
================================================ FILE: test/stubs/_includes/layoutLiquid.liquid ================================================ --- keylayout: valuelayout ---
{{ content }}
================================================ FILE: test/stubs/_includes/layouts/div-wrapper-layout.njk ================================================
{{ content }}
================================================ FILE: test/stubs/_includes/layouts/engineOverrides.njk ================================================ --- layoutkey: layoutvalue ---
{{ content | safe }}
================================================ FILE: test/stubs/_includes/layouts/engineOverridesMd.njk ================================================ --- layoutkey: layoutvalue --- # Layout header
{{ content | safe }}
================================================ FILE: test/stubs/_includes/layouts/inasubdir.njk ================================================ ================================================ FILE: test/stubs/_includes/layouts/issue-115.liquid ================================================ {% for foo in pagination.items -%} {{ foo.data.title }} {% endfor -%} {% for bar in collections.bars -%} {{ bar.data.title }} {% endfor -%} ================================================ FILE: test/stubs/_includes/layouts/layout-contentdump.njk ================================================ --- inherits: a layout: layouts/layout-inherit-b.njk --- {{content | default("this is bad")}} {{inherits}} ================================================ FILE: test/stubs/_includes/layouts/layout-inherit-a.njk ================================================ --- inherits: a layout: layouts/layout-inherit-b.njk --- {{content}} {{inherits}} ================================================ FILE: test/stubs/_includes/layouts/layout-inherit-b.njk ================================================ --- inherits: b secondinherits: b layout: layouts/layout-inherit-c.njk --- {{ content | safe }} {{secondinherits}} ================================================ FILE: test/stubs/_includes/layouts/layout-inherit-c.njk ================================================ --- inherits: c secondinherits: c thirdinherits: c --- {{ content | safe }} {{inherits}} {{thirdinherits}} ================================================ FILE: test/stubs/_includes/layouts/post.liquid ================================================ ================================================ FILE: test/stubs/_includes/layouts/templateMapCollection.njk ================================================ --- upstream: Inherited --- {{ content | safe }} ================================================ FILE: test/stubs/_includes/multiple.liquid ================================================ ================================================ FILE: test/stubs/_includes/multiple.md ================================================ ================================================ FILE: test/stubs/_includes/mylocallayout.njk ================================================
{{ content | safe }}
================================================ FILE: test/stubs/_includes/permalink-data-layout.njk ================================================ --- permalink: "{{ page.fileSlug }}/index.html" --- Wrapper:{{ content | safe }} ================================================ FILE: test/stubs/_includes/permalink-in-layout/layout-fileslug.liquid ================================================ --- permalink: test/{{ page.fileSlug }}/ --- {{ content }} ================================================ FILE: test/stubs/_includes/permalink-in-layout/layout.liquid ================================================ --- permalink: hello/index.html --- {{ content }} ================================================ FILE: test/stubs/_includes/scopeleak.liquid ================================================ {% assign test = 2 %}{{ test }} ================================================ FILE: test/stubs/_includes/subfolder/included.html ================================================ This is an include. ================================================ FILE: test/stubs/_includes/subfolder/included.liquid ================================================ This is an include. ================================================ FILE: test/stubs/_includes/subfolder/included.nunj ================================================ Nunjabusiness2 ================================================ FILE: test/stubs/_includes/test.js ================================================ /* THIS IS A COMMENT */ alert("Issue #398"); ================================================ FILE: test/stubs/_layouts/layoutsdefault.liquid ================================================ ================================================ FILE: test/stubs/add-extension/test.njk ================================================ ================================================ FILE: test/stubs/add-extension/test.txt ================================================ ================================================ FILE: test/stubs/broken-config.cjs ================================================ const missingModule = require("this-is-a-module-that-does-not-exist"); module.exports = function(eleventyConfig) { eleventyConfig.addFilter("cssmin", function(code) { return missingModule(code); }); return {}; }; ================================================ FILE: test/stubs/buffer.11ty.cjs ================================================ module.exports = Buffer.from("

tést

"); ================================================ FILE: test/stubs/cfg-directories-export/eleventy.config.js ================================================ export default function(eleventyConfig) { }; export const config = { dir: { input: "src", includes: "myincludes", data: "mydata", output: "dist" } }; ================================================ FILE: test/stubs/cfg-directories-export/src/.gitkeep ================================================ ================================================ FILE: test/stubs/cfg-directories-export-cjs/eleventy.config.cjs ================================================ module.exports = function(eleventyConfig) { }; module.exports.config = { dir: { input: "src", includes: "myincludes2", data: "mydata2", output: "dist2" } } ================================================ FILE: test/stubs/cfg-directories-export-cjs/src/.gitkeep ================================================ ================================================ FILE: test/stubs/class-async-data-fn.11ty.cjs ================================================ class Test { async data() { return new Promise((resolve, reject) => { setTimeout(function() { resolve({ name: "Ted" }); }, 50); }); } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-async-filter.11ty.cjs ================================================ class Test { static returnsTed() { return "Ted"; } returnsBill() { return "Bill"; } async render({ name }) { return Promise.resolve( `

${this.upper(name)}${this.returnsBill()}${Test.returnsTed()}

` ); } } module.exports = Test; ================================================ FILE: test/stubs/class-async.11ty.cjs ================================================ class Test { async render({ name }) { return Promise.resolve(`

${name}

`); } } module.exports = Test; ================================================ FILE: test/stubs/class-buffer.11ty.cjs ================================================ class Test { returnsBill() { return "Bill"; } static returnsTed() { return "Ted"; } render({ name }) { return Buffer.from( `

${name}${this.returnsBill()}${Test.returnsTed()}

` ); } } module.exports = Test; ================================================ FILE: test/stubs/class-data-filter.11ty.cjs ================================================ class Test { get data() { return { name: "Ted" }; } render({ name }) { return `

${this.upper(name)}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-fn-filter.11ty.cjs ================================================ class Test { data() { return { name: "Ted" }; } render({ name }) { return `

${this.upper(name)}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-fn-shorthand.11ty.cjs ================================================ function Test() {} // this doesn’t return an object?? 🤡 // Test.prototype.data = () => { name: "Ted" }; Test.prototype.data = () => { return { name: "Ted" }; }; Test.prototype.render = ({ name }) => `

${name}

`; /* Test.prototype.data = function() { return { name: "Ted" }; }; Test.prototype.render = function(data) { return `

${data.name}

`; } */ /* // this isn’t valid syntax?? 🤡 class Test { data() => { name: "Ted" }; render({ name }) => `

${name}

`; } */ module.exports = Test; ================================================ FILE: test/stubs/class-data-fn.11ty.cjs ================================================ class Test { data() { return { name: "Ted" }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink-async-fn.11ty.cjs ================================================ class Test { get data() { return { key: "value1", permalink: async function(data) { return new Promise((resolve, reject) => { setTimeout(function() { resolve(`/my-permalink/${data.key}/`); }, 100); }); } }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink-buffer.11ty.cjs ================================================ class Test { get data() { return { permalink: Buffer.from("/my-permalink/") }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink-fn-buffer.11ty.cjs ================================================ class Test { get data() { return { key: "value1", permalink: data => Buffer.from(`/my-permalink/${data.key}/`) }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink-fn-filter.11ty.cjs ================================================ class Test { get data() { return { title: "My Super Cool Title", permalink: function({ title }) { return `/my-permalink/${this.slugify(title)}/`; } }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink-fn.11ty.cjs ================================================ class Test { get data() { return { key: "value1", permalink: data => `/my-permalink/${data.key}/` }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data-permalink.11ty.cjs ================================================ class Test { get data() { return { permalink: "/my-permalink/" }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-data.11ty.cjs ================================================ class Test { get data() { return { name: "Ted" }; } render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-filter.11ty.cjs ================================================ class Test { static returnsTed() { return "Ted"; } returnsBill() { return "Bill"; } render({ name }) { return `

${this.upper( name )}${this.returnsBill()}${Test.returnsTed()}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class-fns-has-page.11ty.cjs ================================================ class TestWithPage { get page() { return "this-is-my-page"; } render(data) { data.avaTest.is(this.page, "this-is-my-page"); data.avaTest.is(data.page.url, "/hi/"); } } module.exports = TestWithPage; ================================================ FILE: test/stubs/class-fns.11ty.cjs ================================================ class Test { render({ avaTest }) { avaTest.truthy(this.url); avaTest.truthy(this.slug); avaTest.truthy(this.log); avaTest.truthy(this.getPreviousCollectionItem); avaTest.truthy(this.getNextCollectionItem); avaTest.truthy(this.page); } } module.exports = Test; ================================================ FILE: test/stubs/class-norender.11ty.cjs ================================================ class Test { data() { return { name: "Ted" }; } } module.exports = Test; ================================================ FILE: test/stubs/class-with-dep-upstream.js ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/class-with-dep.11ty.cjs ================================================ const Dep = require("./class-with-dep-upstream.js"); class Test { returnsBill() { return "Bill"; } static returnsTed() { return "Ted"; } render({ name }) { return `

${name}${this.returnsBill()}${Test.returnsTed()}

`; } } module.exports = Test; ================================================ FILE: test/stubs/class.11ty.cjs ================================================ class Test { returnsBill() { return "Bill"; } static returnsTed() { return "Ted"; } render({ name }) { return `

${name}${this.returnsBill()}${Test.returnsTed()}

`; } } module.exports = Test; ================================================ FILE: test/stubs/classfields-data.11ty.cjs ================================================ class Test { data = { name: "Ted" }; render({ name }) { return `

${name}

`; } } module.exports = Test; ================================================ FILE: test/stubs/cmd-help-processing/_data/test.js ================================================ console.log("THIS SHOULD NOT LOG TO CONSOLE"); module.exports = []; ================================================ FILE: test/stubs/collection/test1.md ================================================ --- title: Test Title tags: - post - dog --- # Test 1 ================================================ FILE: test/stubs/collection/test10.md ================================================ --- title: Test Title tags: - post - office eleventyExcludeFromCollections: - post --- # Test 1 ================================================ FILE: test/stubs/collection/test2.md ================================================ --- tags: cat --- # Test 2 ================================================ FILE: test/stubs/collection/test3.md ================================================ --- tags: - post - cat --- # Test 3 ================================================ FILE: test/stubs/collection/test4.md ================================================ --- date: 1983-01-01 --- # Test 3 ================================================ FILE: test/stubs/collection/test5.md ================================================ --- date: 2038-01-01 --- # Test 3 ================================================ FILE: test/stubs/collection/test6.html ================================================ # Test 6 ================================================ FILE: test/stubs/collection/test7.njk ================================================ # Test 7 ================================================ FILE: test/stubs/collection/test8.md ================================================ --- title: Test Title tags: - post - office eleventyExcludeFromCollections: true --- # Test 1 ================================================ FILE: test/stubs/collection/test9.md ================================================ --- title: Test Title tags: - post - office eleventyExcludeFromCollections: post --- # Test 1 ================================================ FILE: test/stubs/collection-layout/_includes/layout.liquid ================================================ Layout {{ content }} All {{ collections.all | size }} templates Layout {{ collections.dog | size }} dog ================================================ FILE: test/stubs/collection-layout/dog1.liquid ================================================ --- tags: - dog --- ================================================ FILE: test/stubs/collection-layout/template.liquid ================================================ --- layout: layout.liquid --- Template ================================================ FILE: test/stubs/collection-layout-wrap.njk ================================================ --- title: Layout Test layout: layouts/div-wrapper-layout.njk --- {{ title }} ================================================ FILE: test/stubs/collection-slug/dog1.njk ================================================ --- tags: - dog --- ================================================ FILE: test/stubs/collection-slug/template.njk ================================================ fileSlug:{% for post in collections.dog %}{{ post.url }}:{{ post.fileSlug }}{% endfor %} ================================================ FILE: test/stubs/collection-template/_includes/layout.liquid ================================================ Layout {{ content }} ================================================ FILE: test/stubs/collection-template/dog1.liquid ================================================ --- tags: - dog --- ================================================ FILE: test/stubs/collection-template/template.liquid ================================================ --- layout: layout.liquid --- Template All {{ collections.all | size }} templates Template {{ collections.dog | size }} dog ================================================ FILE: test/stubs/collection2/test1.md ================================================ --- title: Test Title tags: - post - dog date: 2009-01-01 --- # Test 1 ================================================ FILE: test/stubs/collection2/test2.md ================================================ --- tags: - post - cat date: 2010-01-01 --- # Test 2 ================================================ FILE: test/stubs/component/component.11tydata.cjs ================================================ const dep2 = require("../deps/dep2.cjs"); module.exports = { localdatakeyfromcjs: "common-js-howdydoody" }; ================================================ FILE: test/stubs/component/component.11tydata.js ================================================ import dep2 from "../deps/dep2.cjs"; export default { localdatakeyfromjs: "howdydoody", }; ================================================ FILE: test/stubs/component/component.11tydata.json ================================================ { "localdatakeyfromjs": "this_is_overridden", "localdatakeyfromcjs": "this_is_overridden", "localdatakeyfromjs2": "howdy2" } ================================================ FILE: test/stubs/component/component.json ================================================ { "localdatakey1": "localdatavalue1", "localdatakeyfromjs": "this_is_also_overridden" } ================================================ FILE: test/stubs/component/component.njk ================================================ {{localdatakey1}} ================================================ FILE: test/stubs/component-async/component.11tydata.cjs ================================================ module.exports = async function() { return new Promise(resolve => { setTimeout(function() { resolve({ localdatakeyfromcjs: "common-js-howdydoody" }); }, 1); }); }; ================================================ FILE: test/stubs/component-async/component.11tydata.js ================================================ export default async function () { return new Promise((resolve) => { setTimeout(function () { resolve({ localdatakeyfromjs: "howdydoody", }); }, 1); }); } ================================================ FILE: test/stubs/component-async/component.njk ================================================ {{localdatakey1}} ================================================ FILE: test/stubs/config-deps-upstream.cjs ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/config-deps.cjs ================================================ const pretty = require("pretty"); const Dep = require("./config-deps-upstream.cjs"); module.exports = function(config) { return {}; }; ================================================ FILE: test/stubs/config-empty-pathprefix.cjs ================================================ module.exports = function (config) { return { pathPrefix: "", }; }; ================================================ FILE: test/stubs/config-promise.js ================================================ module.exports = async function() { return { layouts: "promise" }; }; ================================================ FILE: test/stubs/config.cjs ================================================ const pretty = require("pretty"); module.exports = function (config) { /* { template, inputPath, outputPath, url, data, date } */ return { markdownTemplateEngine: "njk", templateFormats: ["md", "njk"], pathPrefix: "/testdir", keys: { package: "pkg2", }, transforms: { prettyHtml: function (str, outputPath) { if (outputPath.split(".").pop() === "html") { return pretty(str, { ocd: true }); } else { return str; } }, }, nunjucksFilters: { testing: (str) => { return str; }, }, }; }; ================================================ FILE: test/stubs/custom-extension-no-permalink.txt ================================================ Sample content ================================================ FILE: test/stubs/custom-extension.txt ================================================ --- permalink: custom-extension.lit --- Sample content ================================================ FILE: test/stubs/custom-frontmatter/template-excerpt-comment.njk ================================================ --- front: hello --- This is an excerpt. This is content. ================================================ FILE: test/stubs/custom-frontmatter/template-newline1.njk ================================================ --- front: hello --- This is an excerpt.--- This is content. ================================================ FILE: test/stubs/custom-frontmatter/template-newline2.njk ================================================ --- front: hello --- This is an excerpt.---This is content. ================================================ FILE: test/stubs/custom-frontmatter/template-newline3.njk ================================================ --- front: hello --- This is an excerpt. ---This is content. ================================================ FILE: test/stubs/custom-frontmatter/template-nonewline.njk ================================================ --- front: hello --- This is an excerpt. ---This is content. ================================================ FILE: test/stubs/custom-frontmatter/template-toml.njk ================================================ ---toml front = "hello" --- This is content. ================================================ FILE: test/stubs/custom-frontmatter/template.njk ================================================ --- front: hello --- This is an excerpt. --- This is content. ================================================ FILE: test/stubs/data-cascade/template.11tydata.cjs ================================================ module.exports = { parent: { child: 2, datafile: true, }, datafile: true, tags: ["tagC", "tagD", "tagD"], }; ================================================ FILE: test/stubs/data-cascade/template.njk ================================================ --- parent: child: -2 frontmatter: true frontmatter: true tags: - tagA - tagB --- ================================================ FILE: test/stubs/datafiledoesnotexist/template.njk ================================================ ================================================ FILE: test/stubs/dates/2018-01-01-file5.md ================================================ --- tags: dateTestTag --- ================================================ FILE: test/stubs/dates/2019-01-01-folder/2020-01-01-file.md ================================================ --- --- ================================================ FILE: test/stubs/dates/file1.md ================================================ --- tags: dateTestTag --- Assume file created time. ================================================ FILE: test/stubs/dates/file2.md ================================================ --- tags: dateTestTag date: "2016-01-01" --- ================================================ FILE: test/stubs/dates/file2b.md ================================================ --- tags: dateTestTag date: 2016-01-01 --- ================================================ FILE: test/stubs/dates/file3.md ================================================ --- tags: dateTestTag date: Last Modified --- ================================================ FILE: test/stubs/dates/file4.md ================================================ --- tags: dateTestTag date: Created --- ================================================ FILE: test/stubs/default-class-export-and-others.11ty.js ================================================ export default class IndexPage { render(data) { const name = world(); return `
hello
`; } } export function world() { return "World"; } ================================================ FILE: test/stubs/default-export-and-others.11ty.js ================================================ export const foo = "test"; // render export default () => "

hello

"; ================================================ FILE: test/stubs/default-frontmatter.txt ================================================ --- frontmatter: 1 --- hi ================================================ FILE: test/stubs/default-function-export-and-named-data.11ty.cjs ================================================ // render module.exports = (data) => `

${data.name} World

`; module.exports.data = function() { return { name: "Hello" } }; ================================================ FILE: test/stubs/default-function-export-and-named-data.11ty.js ================================================ export function data() { return { name: "Hello" } }; // render export default (data) => `

${data.name} World

`; ================================================ FILE: test/stubs/default-no-liquid.md ================================================ hi ================================================ FILE: test/stubs/default.liquid ================================================ {{ "hi" }} ================================================ FILE: test/stubs/default.md ================================================ {{ "hi" }} ================================================ FILE: test/stubs/dependencies/dep1.cjs ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/dependencies/dep2.cjs ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/dependencies/two-deps.11ty.cjs ================================================ const dep1 = require("./dep1.cjs"); const dep2 = require("./dep2.cjs"); module.exports = ""; ================================================ FILE: test/stubs/deps/dep1.cjs ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/deps/dep2.cjs ================================================ module.exports = function() {}; ================================================ FILE: test/stubs/dynamic-permalink/test.njk ================================================ --- permalink: "/{{justastring}}/" dynamicPermalink: false --- ================================================ FILE: test/stubs/eleventyComputed/first.njk ================================================ --- key1: value1 eleventyComputed: key2: value2-{{ key1 }}.css --- hi:{{ key2 }} ================================================ FILE: test/stubs/eleventyComputed/override-reuse.njk ================================================ --- key1: value1 eleventyComputed: key1: "over({{key1}})ride" --- hi:{{ key1 }} ================================================ FILE: test/stubs/eleventyComputed/override.njk ================================================ --- key1: value1 eleventyComputed: key1: override --- hi:{{ key1 }} ================================================ FILE: test/stubs/eleventyComputed/permalink-simple.njk ================================================ --- key1: value1 eleventyComputed: permalink: "haha-{{key1}}.html" --- hi:{{ key2 }} ================================================ FILE: test/stubs/eleventyComputed/permalink-slug.njk ================================================ --- key1: "This is a string" eleventyComputed: permalink: "haha-{{key1 | slugify}}.html" --- ================================================ FILE: test/stubs/eleventyComputed/permalink.njk ================================================ --- key1: value1 eleventyComputed: permalink: "haha-{{key2}}.html" key2: "{{key1}}" dependsOnPage: "depends:{{page.url}}" nested: key3: "{{key1}}" key4: "depends on computed {{key2}}" --- hi:{{ key2 }} ================================================ FILE: test/stubs/eleventyComputed/second.njk ================================================ ---js { key1: "value1", eleventyComputed: { key3: function(data) { return `value3-${data.key2}`; }, key2: function(data) { return `value2-${data.key1}.css`; } } } --- hi:{{ key2 }} ================================================ FILE: test/stubs/eleventyComputed/third.njk ================================================ ---js { key1: "value1", eleventyComputed: { key1: function(data) { return `value2-${data.key1}`; } } } --- hi:{{ key1 }} ================================================ FILE: test/stubs/eleventyComputed/true.njk ================================================ --- key1: value1 eleventyComputed: key2: true key3: false key4: 324 --- ================================================ FILE: test/stubs/eleventyComputed/use-global-data.njk ================================================ ---js { eleventyComputed: { image: data => { return data.globalData.datakey1; } } } --- Issue #1043 ================================================ FILE: test/stubs/eleventyExcludeFromCollections.njk ================================================ --- title: Paged Test eleventyExcludeFromCollections: true tags: - post - dog --- {{ title }} ================================================ FILE: test/stubs/eleventyExcludeFromCollectionsPermalinkFalse.njk ================================================ --- title: Paged Test eleventyExcludeFromCollections: true permalink: false tags: - post - dog --- {{ title }} ================================================ FILE: test/stubs/engine-singletons/first.njk ================================================ ================================================ FILE: test/stubs/engine-singletons/second.njk ================================================ ================================================ FILE: test/stubs/exitCode/failure.njk ================================================ {{ test() }} ================================================ FILE: test/stubs/exitCode_globalData/_data/test.js ================================================ module.exports = async function () { throw new Error("Testing"); }; ================================================ FILE: test/stubs/exitCode_globalData/test.liquid ================================================ ================================================ FILE: test/stubs/exitCode_success/success.njk ================================================ {{ "hi" }} ================================================ FILE: test/stubs/exports-flatdata.11ty.cjs ================================================ // This is invalid, data must be an object exports.data = "Ted"; exports.render = function(name) { return `

${JSON.stringify(name)}

`; }; ================================================ FILE: test/stubs/fileslug.11ty.cjs ================================================ module.exports = function(data) { return `

${data.page.fileSlug}

`; }; ================================================ FILE: test/stubs/firstdir/seconddir/component.njk ================================================ ================================================ FILE: test/stubs/formatTest.liquid ================================================ --- name: zach ---

{{name | capitalize}}

================================================ FILE: test/stubs/frontmatter-date/test.liquid ================================================ --- mydate: 2009-04-15T12:34:34+01:00 --- {{ mydate | date: "%Y-%m-%d" }} ================================================ FILE: test/stubs/frontmatter-date/test.njk ================================================ --- mydate: 2009-04-15T01:34:34+01:00 --- {{ mydate.toISOString() }} ================================================ FILE: test/stubs/function-arrow.11ty.cjs ================================================ module.exports = ({ name }) => `

${name}

`; ================================================ FILE: test/stubs/function-async-filter.11ty.cjs ================================================ module.exports = async function({ name }) { return `

${await this.upper(name)}

`; }; ================================================ FILE: test/stubs/function-async.11ty.cjs ================================================ module.exports = async function(data) { return new Promise((resolve, reject) => { setTimeout(function() { resolve(`

${data.name}

`); }, 100); }); }; ================================================ FILE: test/stubs/function-buffer.11ty.cjs ================================================ module.exports = function(data) { return Buffer.from(`

${data.name}

`); }; ================================================ FILE: test/stubs/function-filter.11ty.cjs ================================================ function myFunction({ name }) { return `

${this.upper(name)}${myFunction.staticMethod()}

`; } myFunction.staticMethod = function() { return "T9000"; }; module.exports = myFunction; ================================================ FILE: test/stubs/function-fns.11ty.cjs ================================================ module.exports = function({ avaTest }) { avaTest.truthy(this.url); avaTest.truthy(this.slug); avaTest.truthy(this.log); avaTest.truthy(this.getPreviousCollectionItem); avaTest.truthy(this.getNextCollectionItem); avaTest.truthy(this.page); return "test"; }; ================================================ FILE: test/stubs/function-markdown.11ty.cjs ================================================ module.exports = function(data) { return `# ${data.name}`; }; ================================================ FILE: test/stubs/function-prototype.11ty.cjs ================================================ function myFunction() {} myFunction.prototype.render = function({ name }) { return `

${this.upper( name )}${this.returnsBill()}${myFunction.staticMethod()}

`; }; myFunction.prototype.returnsBill = function() { return "Bill"; }; myFunction.staticMethod = function() { return "T9001"; }; module.exports = myFunction; ================================================ FILE: test/stubs/function-throws-async.11ty.cjs ================================================ module.exports = async function(data) { return `

${await this.upper(data.name)}

`; }; ================================================ FILE: test/stubs/function-throws.11ty.cjs ================================================ module.exports = function(data) { return `

${this.upper(data.name)}

`; }; ================================================ FILE: test/stubs/function.11ty.cjs ================================================ module.exports = function(data) { return `

${data.name}

`; }; ================================================ FILE: test/stubs/glob-pages/about.md ================================================ --- title: "About" --- About ================================================ FILE: test/stubs/glob-pages/contact.md ================================================ --- title: "Contact" --- Contact ================================================ FILE: test/stubs/glob-pages/home.md ================================================ --- title: "Home" --- Home ================================================ FILE: test/stubs/global-dash-variable.liquid ================================================ --- is-it-tasty: Yes --- {{ is-it-tasty }} ================================================ FILE: test/stubs/globby/_includes/include.html ================================================ ================================================ FILE: test/stubs/globby/test.html ================================================ ================================================ FILE: test/stubs/ignore-dedupe/.gitignore ================================================ ignoredFolder ignoredFolder ================================================ FILE: test/stubs/ignore1/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignore2/.gitignore ================================================ thisshouldnotexist12345 ================================================ FILE: test/stubs/ignore2/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignore3/.eleventyignore ================================================ ignoredFolder ./ignoredFolder/ignored.md # This is a comment ================================================ FILE: test/stubs/ignore3/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignore4/.eleventyignore ================================================ ignoredFolder ./ignoredFolder/ignored.md # This is a comment ================================================ FILE: test/stubs/ignore4/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignore5/.gitignore ================================================ ================================================ FILE: test/stubs/ignore5/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignore6/.eleventyignore ================================================ ignoredFolder ./ignoredFolder/ignored.md # This is a comment ================================================ FILE: test/stubs/ignore6/.gitignore ================================================ ================================================ FILE: test/stubs/ignore6/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignoredFolder/ignored.md ================================================ # This should be ignored ================================================ FILE: test/stubs/ignorelocalroot/.eleventyignore ================================================ test.md ================================================ FILE: test/stubs/ignorelocalrootgitignore/.eleventyignore ================================================ test.md ================================================ FILE: test/stubs/ignorelocalrootgitignore/.gitignore ================================================ thisshouldnotexist12345 ================================================ FILE: test/stubs/img/stub.md ================================================ ================================================ FILE: test/stubs/included.liquid ================================================ This is not in the includes dir. ================================================ FILE: test/stubs/includer.liquid ================================================

{% include 'included' %}

================================================ FILE: test/stubs/includesemptystring.liquid ================================================ ================================================ FILE: test/stubs/index.html ================================================ This is an html template. ================================================ FILE: test/stubs/index.liquid ================================================ ================================================ FILE: test/stubs/issue-115/index-with-layout.liquid ================================================ --- layout: layouts/issue-115.liquid title: My index page pagination: data: collections.foos size: 12 --- ================================================ FILE: test/stubs/issue-115/index.liquid ================================================ --- title: My index page pagination: data: collections.foos size: 12 --- {% for foo in pagination.items -%} {{ foo.data.title }} {% endfor -%} {% for bar in collections.bars -%} {{ bar.data.title }} {% endfor -%} ================================================ FILE: test/stubs/issue-115/template-bars.liquid ================================================ --- title: This page is bars tags: - bars --- Bars ================================================ FILE: test/stubs/issue-115/template-foos.liquid ================================================ --- title: This page is foos tags: - foos --- Foos. ================================================ FILE: test/stubs/issue-135/template.json ================================================ { "articles": [ { "title": "Do you even paginate bro?", "author": "Bill Ted", "publish_date": "2018-06-22", "tags": [ "post" ], "image": "/url/thing", "teaser": "Teaser copy", "body": "

Raw HTML

" } ] } ================================================ FILE: test/stubs/issue-135/template.njk ================================================ --- pagination: data: articles size: 1 alias: article permalink: blog/{{ article.title | slugify }}/index.html --- {{ article.body | safe }} ================================================ FILE: test/stubs/issue-522/excluded.md ================================================ --- eleventyExcludeFromCollections: true --- # Test {{ collections.all[0].templateContent }} ================================================ FILE: test/stubs/issue-522/template.md ================================================ # Test ================================================ FILE: test/stubs/issue-95/cat.md ================================================ --- tags: - cat --- # Test 8 ================================================ FILE: test/stubs/issue-95/notacat.md ================================================ --- tags: notacat --- # Test 8 ================================================ FILE: test/stubs/layout-permalink-difflang/_includes/test.njk ================================================ --- permalink: "/{{ page.fileSlug }}/" --- {{ content }} ================================================ FILE: test/stubs/layout-permalink-difflang/test.md ================================================ --- layout: test.njk templateEngineOverride: md --- # Title ================================================ FILE: test/stubs/layoutsemptystring.liquid ================================================ ================================================ FILE: test/stubs/local-data-tags/component.11tydata.cjs ================================================ module.exports = { tags: "tag3" }; ================================================ FILE: test/stubs/local-data-tags/component.njk ================================================ --- tags: - tag1 - tag2 --- {{localdatakey1}} ================================================ FILE: test/stubs/multiple-ignores/.eleventyignore ================================================ ignoredFolder ./ignoredFolder/ignored.md # This is a comment ================================================ FILE: test/stubs/multiple-ignores/ignoredFolder/ignored.md ================================================ ================================================ FILE: test/stubs/multiple-ignores/subfolder/.eleventyignore ================================================ ignoredFolder2 ./ignoredFolder2/ignored2.md # This is a comment ================================================ FILE: test/stubs/multiple-ignores/subfolder/ignoredFolder2/ignored2.md ================================================ ================================================ FILE: test/stubs/multipleexports-promises.11ty.cjs ================================================ exports.data = async function() { return new Promise((resolve, reject) => { setTimeout(function() { resolve({ name: "Ted" }); }, 100); }); }; exports.render = async function({ name }) { return new Promise((resolve, reject) => { setTimeout(function() { resolve(`

${name}

`); }, 100); }); }; ================================================ FILE: test/stubs/multipleexports.11ty.cjs ================================================ exports.data = { name: "Ted" }; exports.render = function({ name }) { return `

${name}

`; }; ================================================ FILE: test/stubs/njk-relative/dir/base.njk ================================================

{% block content %}This is a parent.{% endblock %}

================================================ FILE: test/stubs/njk-relative/dir/imports.njk ================================================ {% macro label(text) %}{% endmacro %} ================================================ FILE: test/stubs/njk-relative/dir/included.njk ================================================ HELLO FROM THE OTHER SIDE. ================================================ FILE: test/stubs/njk-relative/dir/unique-include-123.njk ================================================ HELLO FROM THE OTHER SIDE. ================================================ FILE: test/stubs/object-norender.11ty.cjs ================================================ module.exports = { data: { name: "Ted" } }; ================================================ FILE: test/stubs/object.11ty.cjs ================================================ module.exports = { data: { name: "Ted" }, render: function({ name }) { return `

${name}

`; } }; ================================================ FILE: test/stubs/oneinstance.11ty.cjs ================================================ class Test { constructor() { this.rand = Math.random(); } get data() { return { name: "Ted", rand: this.rand }; } render({ name }) { return `

${name}${this.rand}

`; } } module.exports = Test; ================================================ FILE: test/stubs/overrides/layout.njk ================================================ --- templateEngineOverride: liquid title: My Title layout: layouts/engineOverrides.njk ---

{{ title | size }}

================================================ FILE: test/stubs/overrides/layoutfalse.njk ================================================ --- templateEngineOverride: false title: My Title layout: layouts/engineOverrides.njk ---

<%= title %>

================================================ FILE: test/stubs/overrides/page-templatesyntax.md ================================================ --- templateEngineOverride: njk,md --- {{ page.templateSyntax }} ================================================ FILE: test/stubs/overrides/test-bypass.md ================================================ --- templateEngineOverride: njk title: My Title --- # {{ title }} ================================================ FILE: test/stubs/overrides/test-empty.html ================================================ --- templateEngineOverride: title: My Title ---

{{ title }}

================================================ FILE: test/stubs/overrides/test-empty.md ================================================ --- templateEngineOverride: title: My Title --- # {{ title }} ================================================ FILE: test/stubs/overrides/test-error.njk ================================================ --- templateEngineOverride: liquid,njk title: My Title --- # {{ title }} ================================================ FILE: test/stubs/overrides/test-md.liquid ================================================ --- templateEngineOverride: md --- # My Title ================================================ FILE: test/stubs/overrides/test-multiple.md ================================================ --- templateEngineOverride: njk,md title: My Title --- # {{ title }} ================================================ FILE: test/stubs/overrides/test-multiple2.njk ================================================ --- templateEngineOverride: liquid,md title: My Title --- # {{ title }} ================================================ FILE: test/stubs/overrides/test-njk.liquid ================================================ --- templateEngineOverride: njk title: My Title --- {{ title }} ================================================ FILE: test/stubs/overrides/test.html ================================================ --- templateEngineOverride: njk title: My Title ---

{{ title }}

================================================ FILE: test/stubs/overrides/test.liquid ================================================ --- templateEngineOverride: njk title: My Title --- {{ title }} ================================================ FILE: test/stubs/overrides/test.md ================================================ --- title: My Title layout: layouts/engineOverridesMd.njk --- # {{ title }} ================================================ FILE: test/stubs/page-target-collections/paginateall.njk ================================================ --- pagination: data: collections.all size: 1 alias: entry filter: - all addAllPagesToCollections: true --- INPUT PATH:{{ entry.inputPath }} ================================================ FILE: test/stubs/page-target-collections/tagpages.njk ================================================ --- pagination: data: collections size: 1 alias: tag filter: - all --- {{ tag }} ================================================ FILE: test/stubs/page-target-collections/tagpagesall.njk ================================================ --- pagination: data: collections size: 1 alias: tag filter: - all addAllPagesToCollections: true --- {{ tag }} ================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/consumer.njk ================================================ {% for item in collections.pagingtag %}{{ item.templateContent | safe }}{% endfor %} ================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/paged-downstream.njk ================================================ --- pagination: data: collections.pagingtag size: 1 ---
    {% for item in pagination.items %}
  1. {{ item.url }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/paged-main.njk ================================================ --- pagination: data: collections.tag1 size: 2 addAllPagesToCollections: true tags: - pagingtag ---
    {% for item in pagination.items %}
  1. {{ item.url }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/test1.njk ================================================ --- title: Testing 1 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/test2.njk ================================================ --- title: Testing 2 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/cfg-collection-tag-cfg-collection/test3.njk ================================================ --- title: Testing 3 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection/consumer.njk ================================================ {% for item in collections.pagingtag %}{{ item.templateContent | safe }}{% endfor %} ================================================ FILE: test/stubs/paged/collection/main.njk ================================================ --- pagination: data: collections.tag1 size: 2 tags: - pagingtag ---
    {% for item in pagination.items %}
  1. {{ item.url }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/collection/test1.njk ================================================ --- title: Testing 1 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection/test2.njk ================================================ --- title: Testing 2 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection/test3.njk ================================================ --- title: Testing 3 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection-apply-to-all/consumer.njk ================================================ {% for item in collections.pagingtag %}{{ item.templateContent | safe }}{% endfor %} ================================================ FILE: test/stubs/paged/collection-apply-to-all/main.njk ================================================ --- pagination: data: collections.tag1 size: 2 addAllPagesToCollections: true tags: - pagingtag ---
    {% for item in pagination.items %}
  1. {{ item.url }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/collection-apply-to-all/test1.njk ================================================ --- title: Testing 1 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection-apply-to-all/test2.njk ================================================ --- title: Testing 2 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/collection-apply-to-all/test3.njk ================================================ --- title: Testing 3 tags: - tag1 --- {{ title }} ================================================ FILE: test/stubs/paged/notpaged.njk ================================================ --- testdata: sub: - item1 - item2 - item3 - item4 - item5 - item6 - item7 - item8 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-before-and-reverse.njk ================================================ ---js { pagination: { data: "items", size: 1, reverse: true, before: function(data) { return data.slice(0, 2); } }, items: ["item1", "item2", "item3", "item4", "item5", "item6"] } ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-before-filter.njk ================================================ ---js { pagination: { data: "items", size: 1, before: function(data) { return data.slice(0, 2).reverse(); }, alias: "myalias" }, items: ["item1", "item2", "item3", "item4", "item5", "item6"] } ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-before-metadata.njk ================================================ ---js { keyword: "item3", pagination: { data: "items", size: 1, before: function(data, metadata) { return data.filter(el => el === metadata.keyword); } }, items: ["item1", "item2", "item3", "item4", "item5", "item6"] } ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-before.njk ================================================ ---js { pagination: { data: "items", size: 1, before: function(data) { return data.reverse(); }, alias: 'myalias' }, items: ["item1", "item2", "item3", "item4", "item5", "item6"] } ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-empty-pageonemptydata.njk ================================================ --- pagination: data: items size: 1 generatePageOnEmptyData: true items: [] ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged-empty.njk ================================================ --- pagination: data: items size: 1 items: [] ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/paged.json ================================================ { "items": { "sub": [ "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8" ] } } ================================================ FILE: test/stubs/paged/paged.njk ================================================ --- pagination: data: items.sub size: 5 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedalias.njk ================================================ --- pagination: data: items size: 1 alias: font.test items: - item1 - item2 permalink: pagedalias/{{ font.test }}/index.html --- {{ font.test }} ================================================ FILE: test/stubs/paged/pagedaliassize2.njk ================================================ --- pagination: data: items size: 2 alias: font.test items: - item1 - item2 - item3 - item4 permalink: pagedalias/{{ font.test[0] }}/index.html --- {{ font.test[0] }} ================================================ FILE: test/stubs/paged/pagedinlinedata-reverse.njk ================================================ --- pagination: data: testdata size: 4 reverse: true testdata: - item1 - item2 - item3 - item4 - item5 - item6 - item7 - item8 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedinlinedata.njk ================================================ --- pagination: data: testdata size: 4 testdata: - item1 - item2 - item3 - item4 - item5 - item6 - item7 - item8 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedobject.njk ================================================ --- pagination: data: testdata size: 4 testdata: item1: itemvalue1 item2: itemvalue2 item3: itemvalue3 item4: itemvalue4 item5: itemvalue5 item6: itemvalue6 item7: itemvalue7 item8: itemvalue8 item9: itemvalue9 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedobjectfilterarray.njk ================================================ --- pagination: data: testdata size: 4 filter: - item4 testdata: item1: itemvalue1 item2: itemvalue2 item3: itemvalue3 item4: itemvalue4 item5: itemvalue5 item6: itemvalue6 item7: itemvalue7 item8: itemvalue8 item9: itemvalue9 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedobjectfilterstring.njk ================================================ --- pagination: data: testdata size: 4 filter: item4 testdata: item1: itemvalue1 item2: itemvalue2 item3: itemvalue3 item4: itemvalue4 item5: itemvalue5 item6: itemvalue6 item7: itemvalue7 item8: itemvalue8 item9: itemvalue9 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedobjectvalues.njk ================================================ --- pagination: data: testdata size: 4 resolve: values testdata: item1: itemvalue1 item2: itemvalue2 item3: itemvalue3 item4: itemvalue4 item5: itemvalue5 item6: itemvalue6 item7: itemvalue7 item8: itemvalue8 item9: itemvalue9 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedpermalink.njk ================================================ --- pagination: data: items size: 5 items: - Slug CANDIDATE - item2 - item3 - item4 - item5 - another-slug CandiDate - item7 - item8 permalink: paged/{{ pagination.items[0] | slugify }}/index.html ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedpermalinkif.liquid ================================================ --- pagination: data: items size: 2 items: - item1 - item2 - item3 - item4 permalink: paged/{% if pagination.pageNumber > 0 %}page-{{ pagination.pageNumber }}/{% endif %}index.html ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedpermalinkif.njk ================================================ --- pagination: data: items size: 2 items: - item1 - item2 - item3 - item4 permalink: paged/{% if pagination.pageNumber > 0 %}page-{{ pagination.pageNumber }}/{% endif %}index.html ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedpermalinknumeric.njk ================================================ --- pagination: data: items size: 5 items: - Slug CANDIDATE - item2 - item3 - item4 - item5 - another-slug CandiDate - item7 - item8 permalink: paged/page-{{ pagination.pageNumber }}/index.html ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedpermalinknumericoneindexed.njk ================================================ --- pagination: data: items size: 5 items: - Slug CANDIDATE - item2 - item3 - item4 - item5 - another-slug CandiDate - item7 - item8 permalink: paged/page-{{ pagination.pageNumber + 1 }}/index.html ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged/pagedresolve.njk ================================================ --- pagination: data: testdata.sub size: 4 testdata: sub: - item1 - item2 - item3 - item4 - item5 - item6 - item7 - item8 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/paged-global-data-mutable/_data/testdata.cjs ================================================ module.exports = [ { key1: "item1", key2: "item2", }, { key3: "item3", key4: "item4", }, { key5: "item5", key6: "item6", }, ]; ================================================ FILE: test/stubs/paged-global-data-mutable/paged-differing-data-set.njk ================================================ --- pagination: data: testdata size: 1 alias: item --- 1:{{ item.key1 }} 2:{{ item.key2 }} 3:{{ item.key3 }} 4:{{ item.key4 }} 5:{{ item.key5 }} 6:{{ item.key6 }} ================================================ FILE: test/stubs/pagedate.liquid ================================================ {{ page.date }} ================================================ FILE: test/stubs/pagedate.njk ================================================ {{ page.date }} ================================================ FILE: test/stubs/pagedateutc.njk ================================================ {{ page.date.toUTCString() }} ================================================ FILE: test/stubs/pagination-eleventycomputed-permalink.liquid ================================================ --- venues: - first - second - third pagination: data: venues size: 1 alias: venue addAllPagesToCollections: true eleventyComputed: permalink: "venues/{{ venue }}/" --- ================================================ FILE: test/stubs/pagination-eleventycomputed-title.liquid ================================================ --- venues: - id: 1 name: first - id: 2 name: second - id: 3 name: third randommetadata: "woopers" pagination: data: venues size: 1 alias: venue eleventyComputed: title: "website - {{ venue.name }}" --- {{ venue.name }} {{ randommetadata }} ================================================ FILE: test/stubs/pagination-templatecontent/index.njk ================================================ --- pagination: data: collections.post size: 5 alias: posts --- {%- for post in posts -%} {{ post.templateContent | safe }} {%- endfor -%} ================================================ FILE: test/stubs/pagination-templatecontent/post-1.md ================================================ --- tags: - post --- # Post 1 ================================================ FILE: test/stubs/pagination-templatecontent/post-2.md ================================================ --- tags: - post --- # Post 2 ================================================ FILE: test/stubs/permalink-build/permalink-build.md ================================================ --- permalink: build: /url/ --- This should be the same as `permalink: /url/` ================================================ FILE: test/stubs/permalink-conflicts/test1.md ================================================ --- title: Test Title tags: - post - dog permalink: /permalink-conflicts/ --- # Test 1 ================================================ FILE: test/stubs/permalink-conflicts/test2.md ================================================ --- title: Test Title tags: - post - dog permalink: /permalink-conflicts/ --- # Test 2 ================================================ FILE: test/stubs/permalink-conflicts/test3.md ================================================ --- title: Test Title tags: - post - dog permalink: permalink-conflicts/ --- # Test 3 ================================================ FILE: test/stubs/permalink-conflicts-false/test1.md ================================================ --- title: Test Title tags: - post - dog permalink: false --- # Test 1 ================================================ FILE: test/stubs/permalink-conflicts-false/test2.md ================================================ --- title: Test Title tags: - post - dog permalink: false --- # Test 2 ================================================ FILE: test/stubs/permalink-data-layout/test.json ================================================ { "layout": "permalink-data-layout.njk" } ================================================ FILE: test/stubs/permalink-data-layout/test.njk ================================================ Test 1:{{ page.fileSlug }} ================================================ FILE: test/stubs/permalink-empty-object/empty-object.md ================================================ --- permalink: {} --- This should be the same as if permalink was not set at all. ================================================ FILE: test/stubs/permalink-false/test.md ================================================ --- permalink: false --- This shouldn’t write ================================================ FILE: test/stubs/permalink-false-computed/test.md ================================================ --- eleventyComputed: permalink: false --- This shouldn’t write ================================================ FILE: test/stubs/permalink-in-layout-fileslug.liquid ================================================ --- layout: permalink-in-layout/layout-fileslug.liquid --- Current url: {{ permalink }} ================================================ FILE: test/stubs/permalink-in-layout.liquid ================================================ --- layout: permalink-in-layout/layout.liquid --- Current url: {{ permalink }} ================================================ FILE: test/stubs/permalink-markdown-override.md ================================================ --- title: My Title permalink: /news/my-test-file/index.html templateEngineOverride: html,md --- # <%= title %> ================================================ FILE: test/stubs/permalink-markdown-var.md ================================================ --- title: My Title permalink: /news/{{ title | slugify }}/index.html --- # <%= title %> ================================================ FILE: test/stubs/permalink-markdown.md ================================================ --- title: My Title permalink: /news/my-test-file/index.html --- # <%= title %> ================================================ FILE: test/stubs/permalink-nobuild/permalink-nobuild.md ================================================ --- permalink: someotherkey: /url/ --- This shouldn’t write to the file system or be rendered by a template engine. ================================================ FILE: test/stubs/permalink-true/permalink-true.md ================================================ --- permalink: true --- This should throw an error. ================================================ FILE: test/stubs/permalinkdata-jsfn.njk ================================================ ---js { title: "slug", permalink: "subdir/{{title}}/" } --- Slugged. ================================================ FILE: test/stubs/permalinkdata-jspermalinkfn.njk ================================================ ---js { title: "slug", permalink: function(data) { return `subdir/${data.title}/`; } } --- Slugged. ================================================ FILE: test/stubs/permalinkdata.njk ================================================ --- title: Slug CANDIDATE permalink: subdir/{{ title | slugify }}/index.html --- Slugged. ================================================ FILE: test/stubs/permalinkdate.liquid ================================================ --- title: Date Permalink date: "2016-01-01T06:00-06:00" permalink: "/{{ page.date | date: '%Y/%m/%d' }}/index.html" --- Date Permalinks ================================================ FILE: test/stubs/permalinked.liquid ================================================ --- permalink: permalinksubfolder/index.html --- Current url: {{ permalink }} ================================================ FILE: test/stubs/posts/post1.njk ================================================ Post1 ================================================ FILE: test/stubs/posts/posts.json ================================================ { "layout": "mylocallayout.njk" } ================================================ FILE: test/stubs/posts/posts.njk ================================================ Posts ================================================ FILE: test/stubs/prematureTemplateContent/test.11ty.cjs ================================================ module.exports = function(data) { return data.collections.all[0].templateContent; }; ================================================ FILE: test/stubs/prematureTemplateContent/test.liquid ================================================ {{ collections.all[0].templateContent }} ================================================ FILE: test/stubs/prematureTemplateContent/test.md ================================================ {{ sample.templateContent }} ================================================ FILE: test/stubs/prematureTemplateContent/test.njk ================================================ {{ sample.templateContent }} ================================================ FILE: test/stubs/promise.11ty.cjs ================================================ module.exports = new Promise((resolve, reject) => { setTimeout(function() { resolve("

Zach

"); }, 100); }); ================================================ FILE: test/stubs/public/test.css ================================================ ================================================ FILE: test/stubs/relative-liquid/dir/included.liquid ================================================ TIME IS RELATIVE. ================================================ FILE: test/stubs/reuse-permalink/reuse-permalink.json ================================================ { "permalink": "/{{ page.date | date: '%Y/%m/%d' }}/index.html" } ================================================ FILE: test/stubs/reuse-permalink/test1.liquid ================================================ --- title: Test Title date: "2016-01-01T06:00-06:00" --- # Test 1 ================================================ FILE: test/stubs/script-frontmatter/test-default.njk ================================================ --- import {noopSync} from "@zachleat/noop"; const myString = "Hi"; // export a function function myFunction() { return "Bye" } ---
{{ noopSync(myString) }}
{{ myFunction() }}
================================================ FILE: test/stubs/script-frontmatter/test-js.njk ================================================ ---javascript { myFunction: function() { return "HELLO!"; } } ---
{{ myFunction() }}
================================================ FILE: test/stubs/script-frontmatter/test.njk ================================================ ---js import {noopSync} from "@zachleat/noop"; const myString = "Hi"; // export a function function myFunction() { return "Bye" } ---
{{ noopSync(myString) }}
{{ myFunction() }}
================================================ FILE: test/stubs/string.11ty.cjs ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/string.11ty.custom ================================================ export default "

Zach

"; ================================================ FILE: test/stubs/string.11ty.possum ================================================ export default "

Possum

"; ================================================ FILE: test/stubs/stubs-1541/_includes/render-source.liquid ================================================ {{ page.url }} via {{ eleventy.env.source }} collections.all size: {{ collections.all | size }} ================================================ FILE: test/stubs/stubs-computed-permalink/eleventycomputed-nested-object.11ty.cjs ================================================ module.exports.data = { lang: "en", eleventyComputed: { permalink: function (data) { // console.log(">>>>", { data }); return { build: `/i18n/${data.lang}/`, }; }, }, }; ================================================ FILE: test/stubs/stubs-computed-permalink/eleventycomputed-object-replace.11ty.cjs ================================================ module.exports.data = { lang: "en", permalink: "/i18n/{{lang}}/", eleventyComputed: { permalink: function (data) { return { build: data.permalink.replace("{{lang}}", "en"), }; }, }, }; ================================================ FILE: test/stubs/stubs-computed-permalink/eleventycomputed-object.11ty.cjs ================================================ module.exports.data = { lang: "en", eleventyComputed: { permalink: { build: function (data) { return `/i18n/${data.lang}/`; }, }, }, }; ================================================ FILE: test/stubs/stubs-virtual-conflict/virtual.md ================================================ # This is on the file system ================================================ FILE: test/stubs/subdir/img/.gitkeep ================================================ ================================================ FILE: test/stubs/subdir/index.html ================================================ ================================================ FILE: test/stubs/subfolder/index.html ================================================ ================================================ FILE: test/stubs/subfolder/subfolder/subfolder.liquid ================================================ ================================================ FILE: test/stubs/subfolder/subfolder.liquid ================================================ subfolder ================================================ FILE: test/stubs/tagged-pagination-multiples/test.njk ================================================ --- title: Paged Test tags: - haha pagination: data: collections.userCollection size: 1 alias: item addAllPagesToCollections: true --- {{ title }} ================================================ FILE: test/stubs/tagged-pagination-multiples-layout/test.njk ================================================ --- title: Paged Test layout: layouts/div-wrapper-layout.njk testdata: - one - two - three pagination: data: testdata size: 1 alias: item addAllPagesToCollections: true --- {{ item }} ================================================ FILE: test/stubs/template-passthrough/.htaccess ================================================ ================================================ FILE: test/stubs/template-passthrough/static/nested/test-nested.css ================================================ ================================================ FILE: test/stubs/template-passthrough/static/test.css ================================================ ================================================ FILE: test/stubs/template-passthrough/static/test.js ================================================ ================================================ FILE: test/stubs/template-passthrough2/.htaccess ================================================ ================================================ FILE: test/stubs/template-passthrough2/static/nested/test-nested.css ================================================ ================================================ FILE: test/stubs/template-passthrough2/static/test.css ================================================ ================================================ FILE: test/stubs/template-passthrough2/static/test.js ================================================ ================================================ FILE: test/stubs/template.liquid ================================================ ================================================ FILE: test/stubs/templateFrontMatter.liquid ================================================ --- key1: value1 key2: value2 key3: value3 --- c:{{ key1 }}:{{ key2 }}:{{ key3 }} ================================================ FILE: test/stubs/templateFrontMatterJs.njk ================================================ ---js { key1: "value1", key2: function(value) { return value.toUpperCase(); }, key3: "value3" } --- c:{{ key1 }}:{{ key2("value2") }}:{{ key3 }} ================================================ FILE: test/stubs/templateFrontMatterJson.liquid ================================================ ---json { "key1": "value1", "key2": "value2", "key3": "value3" } --- c:{{ key1 }}:{{ key2 }}:{{ key3 }} ================================================ FILE: test/stubs/templateLayoutCacheDuplicates/_includes/layout.njk ================================================ Hello A ================================================ FILE: test/stubs/templateLayoutCacheDuplicates-b/_includes/layout.njk ================================================ Hello B ================================================ FILE: test/stubs/templateMapCollection/paged-cfg-permalink.md ================================================ --- title: Paged Test pagination: data: collections.userCollection size: 1 alias: item permalink: /{{ item.data.title | slugify }}/hello/ --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-cfg-tagged-apply-to-all.md ================================================ --- title: Paged Test tags: - haha pagination: data: collections.userCollection size: 1 alias: item addAllPagesToCollections: true --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-cfg-tagged-permalink-apply-to-all.md ================================================ --- title: Paged Test tags: - haha pagination: data: collections.userCollection size: 1 alias: item addAllPagesToCollections: true permalink: /{{ item.data.title | slugify }}/goodbye/ --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-cfg-tagged-permalink.md ================================================ --- title: Paged Test tags: - haha pagination: data: collections.userCollection size: 1 alias: item permalink: /{{ item.data.title | slugify }}/goodbye/ --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-cfg-tagged.md ================================================ --- title: Paged Test tags: - haha pagination: data: collections.userCollection size: 1 alias: item --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-cfg.md ================================================ --- title: Paged Test pagination: data: collections.userCollection size: 1 --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/paged-tag-dogs-templateContent-alias.md ================================================ --- title: Paged Test pagination: data: collections.dog size: 2 alias: dogs --- Before {% for dog in dogs %} {{ dog.templateContent }} {% endfor %} After ================================================ FILE: test/stubs/templateMapCollection/paged-tag-dogs-templateContent.md ================================================ --- title: Paged Test pagination: data: collections.dog size: 1 --- Before {% for dog in pagination.items %} {{ dog.templateContent }} {% endfor %} After ================================================ FILE: test/stubs/templateMapCollection/paged-tag.md ================================================ --- title: Paged Test pagination: data: collections.dog size: 1 --- # {{ title }} ================================================ FILE: test/stubs/templateMapCollection/templateContent.md ================================================ --- tags: circle --- # Test {{ collections.circle[0].templateContent }} ================================================ FILE: test/stubs/templateMapCollection/test1.md ================================================ --- title: Test Title tags: - post - dog --- # Test 1 ================================================ FILE: test/stubs/templateMapCollection/test2.md ================================================ --- tags: cat --- # Test 2 ================================================ FILE: test/stubs/templateMapCollection/test3.md ================================================ --- tags: circle --- # {{ collections.circle.length }}, {{ collections.circle[0].url }} ================================================ FILE: test/stubs/templateMapCollection/test4.md ================================================ --- title: Test Title 4 tags: - post - dog --- # Test 4 ================================================ FILE: test/stubs/templateMapCollection/test5.md ================================================ --- title: Test Title 5 --- # Test 5 ================================================ FILE: test/stubs/templateMapCollection/testWithLayout.md ================================================ --- layout: layouts/templateMapCollection.njk --- {{ upstream }} ================================================ FILE: test/stubs/templateTwoLayouts.liquid ================================================ --- layout: layout-a key1: value1 title: 'Font Aliasing, or How to Rename a Font in CSS' permalink: /rename-font/ postRank: 4 daysPosted: 152 yearsPosted: 0.4 ---

{{ upstream }}

================================================ FILE: test/stubs/templateWithLayout.liquid ================================================ --- layout: layoutLiquid.liquid keymain: valuemain title: 'Font Aliasing, or How to Rename a Font in CSS' permalink: /rename-font/ ---

Hello.

================================================ FILE: test/stubs/templateWithLayoutContent.liquid ================================================ --- layout: defaultLayoutLayoutContent keymain: valuemain title: 'Font Aliasing, or How to Rename a Font in CSS' permalink: /rename-font/ ---

Hello.

================================================ FILE: test/stubs/templateWithLayoutKey.liquid ================================================ --- layout: defaultLayout keymain: valuemain title: 'Font Aliasing, or How to Rename a Font in CSS' permalink: /rename-font/ ---

Hello.

================================================ FILE: test/stubs/templatetest-frontmatter/multiple.njk ================================================ --- tags: - multi-tag - multi-tag-2 --- {% if tags.includes("multi-tag-2") %}Has multi-tag-2{% endif %} ================================================ FILE: test/stubs/templatetest-frontmatter/single.njk ================================================ --- tags: single-tag --- {% if tags.includes("single-tag") %}Has single-tag{% endif %} ================================================ FILE: test/stubs/test-override-js-markdown.11ty.cjs ================================================ class Test { data() { return { name: "markdown", templateEngineOverride: "11ty.js,md" }; } render(data) { return `# This is ${data.name}`; } } module.exports = Test; ================================================ FILE: test/stubs/testing.html ================================================ This is an html template. ================================================ FILE: test/stubs/transform-pages/template.njk ================================================ --- pagination: data: testdata size: 4 testdata: - item1 - item2 - item3 - item4 - item5 - item6 - item7 - item8 ---
    {% for item in pagination.items %}
  1. {{ item }}
  2. {% endfor %}
================================================ FILE: test/stubs/use-collection.11ty.cjs ================================================ module.exports = function({ collections }) { return `
    ${collections.post .map(post => `
  • ${post.data.title}
  • `) .join("")}
`; }; ================================================ FILE: test/stubs/vue-layout.11ty.cjs ================================================ const { createSSRApp } = require("vue"); const { renderToString } = require("@vue/server-renderer"); module.exports = async function (data) { var app = createSSRApp({ template: "

Hello {{ data.name }}, this is a Vue template.

", data: function () { return { data }; }, // components: { // 'test': ComponentA // } }); let content = await renderToString(app, { title: "Test" }); return ` Test ${content}`; }; ================================================ FILE: test/stubs/vue.11ty.cjs ================================================ const { createSSRApp } = require("vue"); const { renderToString } = require("@vue/server-renderer"); module.exports = async function (templateData) { var app = createSSRApp({ template: "

Hello {{ data.name }}, this is a Vue template.

", data: function () { return { data: templateData, }; }, // components: { // 'test': ComponentA // } }); return renderToString(app); }; ================================================ FILE: test/stubs/writeTest/test.md ================================================ # Header ================================================ FILE: test/stubs/writeTestJS/sample.cjs ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/writeTestJS/test.11ty.cjs ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/writeTestJS-casesensitive/sample.Js ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/writeTestJS-casesensitive/test.11Ty.js ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/writeTestJS-passthrough/sample.js ================================================ export default "

Zach

"; ================================================ FILE: test/stubs/writeTestJS-passthrough/test.11ty.js ================================================ export default "

Zach

"; ================================================ FILE: test/stubs/writeTestMarkdown/sample.md ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs/writeTestMarkdown/sample2.markdown ================================================ module.exports = "

Zach

"; ================================================ FILE: test/stubs--to/test.md ================================================ # hi ================================================ FILE: test/stubs--to/test2.liquid ================================================ --- hi: hello --- {{ hi }} ================================================ FILE: test/stubs-1206/page1.njk ================================================ --- tags: tag1 --- This is the first template.{{ page.rawInput }} ================================================ FILE: test/stubs-1206/page2.njk ================================================ This is the second template.{{ collections.tag1[0].rawInput }} ================================================ FILE: test/stubs-1242/_data/xyz.dottest/test.json ================================================ { "abc": 42 } ================================================ FILE: test/stubs-1242/_data/xyz.dottest.json ================================================ { "hi": "bye" } ================================================ FILE: test/stubs-1242/empty.md ================================================ ================================================ FILE: test/stubs-1325/test.11ty.js ================================================ ================================================ FILE: test/stubs-1325/test.js ================================================ ================================================ FILE: test/stubs-142/index.njk ================================================ --- date: git Last Modified --- {{ page.date.getTime() }} ================================================ FILE: test/stubs-1691/_data/str.txt ================================================ Testing ================================================ FILE: test/stubs-1691/template.11tydata.txt ================================================ Template Data File ================================================ FILE: test/stubs-1691/template.njk ================================================ ================================================ FILE: test/stubs-2145/_includes/layout.njk ================================================ --- LayoutData: 123 --- FromLayout{{ content }} ================================================ FILE: test/stubs-2145/test.njk ================================================ {{ layout }} ================================================ FILE: test/stubs-2167/paginated.njk ================================================ --- dropdown: - a - b - c - d - e pagination: data: dropdown size: 1 permalink: false --- ================================================ FILE: test/stubs-2224/index.njk ================================================ --- date: git created --- {{ page.date.getTime() }} ================================================ FILE: test/stubs-2258/_includes/_code.scss ================================================ code { padding: 0.25em; line-height: 0; } ================================================ FILE: test/stubs-2258/_includes/layout.njk ================================================ /* Banner */ {{ content | safe }} ================================================ FILE: test/stubs-2258/eleventy.config.cjs ================================================ const path = require("path"); const sass = require("sass"); module.exports = function (eleventyConfig) { eleventyConfig.addTemplateFormats("scss"); eleventyConfig.addExtension("scss", { outputFileExtension: "css", // optional, default: "html" compile: function (inputContent, inputPath) { let parsed = path.parse(inputPath); let dirs = [ parsed.dir || ".", // BRITTLE TEST ALERT: DON’T REUSE THIS CODE // it expects the template to be in the project root dir path.join(parsed.dir, this.config.dir.includes), ]; let result = sass.compileString(inputContent, { loadPaths: dirs, }); this.addDependencies(inputPath, result.loadedUrls); return (data) => { return result.css; }; }, }); }; ================================================ FILE: test/stubs-2258/style.scss ================================================ --- layout: layout.njk --- @use "code.scss"; /* Comment */ ================================================ FILE: test/stubs-2258-2830-skip-layouts/_includes/layout.njk ================================================ /* Banner */ {{ content | safe }} ================================================ FILE: test/stubs-2258-2830-skip-layouts/eleventy.config.cjs ================================================ const path = require("path"); const sass = require("sass"); module.exports = function (eleventyConfig) { eleventyConfig.addTemplateFormats("scss"); eleventyConfig.addExtension("scss", { useLayouts: false, outputFileExtension: "css", // optional, default: "html" compile: function (inputContent, inputPath) { let parsed = path.parse(inputPath); let dirs = [ parsed.dir || ".", // BRITTLE TEST ALERT: DON’T REUSE THIS CODE // it expects the template to be in the project root dir path.join(parsed.dir, this.config.dir.includes), ]; let result = sass.compileString(inputContent, { loadPaths: dirs, }); return (data) => { return result.css; }; }, }); }; ================================================ FILE: test/stubs-2258-2830-skip-layouts/style.scss ================================================ --- layout: layout.njk --- code { padding: 0.25em; line-height: 0; } ================================================ FILE: test/stubs-2261/_includes/block.njk ================================================ {% macro block() %}
{{ caller() }}
{% endmacro %} ================================================ FILE: test/stubs-2261/eleventy.config.js ================================================ export default function(eleventyConfig) { eleventyConfig.addPairedShortcode("sample", function(content, firstName) { return `${content} ${firstName}` }); }; ================================================ FILE: test/stubs-2261/index.njk ================================================ {% from "block.njk" import block with context %} {% call block() %}Hello{% sample "Manuel" %}Hello{% endsample %}{% endcall %} ================================================ FILE: test/stubs-2367/_includes/layout.liquid ================================================ --- text: layout url: "/mylayout" --- {% simplelink text url text url text url %} {% simplelink text, url, text, url, text, url %} ================================================ FILE: test/stubs-2367/templateWithLiquidShortcodeMultipleArguments-template2.liquid ================================================ --- layout: layout.liquid --- ================================================ FILE: test/stubs-2367/templateWithLiquidShortcodeMultipleArguments.liquid ================================================ --- layout: layout.liquid --- ================================================ FILE: test/stubs-2602/index.njk ================================================ --- permalink: /deep/ --- Home Test Test ================================================ FILE: test/stubs-2753/_data/global.js ================================================ let count = 0; export default async function () { return ++count; }; ================================================ FILE: test/stubs-2753/page1.njk ================================================ {{ global }} ================================================ FILE: test/stubs-2753/page2.njk ================================================ {{ global }} ================================================ FILE: test/stubs-2790/page.11ty.cjs ================================================ module.exports = function ({ name }) { return `

${this.jsfunction(name)}

`; }; ================================================ FILE: test/stubs-2851/content.njk ================================================ --- tags: ['tag with spaces'] --- ================================================ FILE: test/stubs-2851/paginated.njk ================================================ --- pagination: data: "collections['tag with spaces']" size: 1 --- ================================================ FILE: test/stubs-3013/html/_data/books.json ================================================ [ { "id": 1, "name": "The Effervescent adventures of Paul Mescal", "shortname": "paul-mescal" }, { "id": 2, "name": "Populace and Power: A user's guide", "shortname": "populace-and-power" } ] ================================================ FILE: test/stubs-3013/html/_includes/base.html ================================================ --- title: Books --- {{ title }} ================================================ FILE: test/stubs-3013/html/book.html ================================================ --- layout: base pagination: data: books size: 1 alias: book permalink: /{{ book.shortname }}/ eleventyComputed: title: "{{ book.name }}" --- {{ title }} ================================================ FILE: test/stubs-3013/liquid/_data/books.json ================================================ [ { "id": 1, "name": "The Effervescent adventures of Paul Mescal", "shortname": "paul-mescal" }, { "id": 2, "name": "Populace and Power: A user's guide", "shortname": "populace-and-power" } ] ================================================ FILE: test/stubs-3013/liquid/_includes/base.liquid ================================================ --- title: Books --- {{ title }} ================================================ FILE: test/stubs-3013/liquid/book.liquid ================================================ --- layout: base pagination: data: books size: 1 alias: book permalink: /{{ book.shortname }}/ eleventyComputed: title: "{{ book.name }}" --- {{ title }} ================================================ FILE: test/stubs-3013/md/_data/books.json ================================================ [ { "id": 1, "name": "The Effervescent adventures of Paul Mescal", "shortname": "paul-mescal" }, { "id": 2, "name": "Populace and Power: A user's guide", "shortname": "populace-and-power" } ] ================================================ FILE: test/stubs-3013/md/_includes/base.md ================================================ --- title: Books --- {{ title }} ================================================ FILE: test/stubs-3013/md/book.md ================================================ --- layout: base pagination: data: books size: 1 alias: book permalink: /{{ book.shortname }}/ eleventyComputed: title: "{{ book.name }}" --- {{ title }} ================================================ FILE: test/stubs-3013/njk/_data/books.json ================================================ [ { "id": 1, "name": "The Effervescent adventures of Paul Mescal", "shortname": "paul-mescal" }, { "id": 2, "name": "Populace and Power: A user's guide", "shortname": "populace-and-power" } ] ================================================ FILE: test/stubs-3013/njk/_includes/base.njk ================================================ --- title: Books --- {{ title }} ================================================ FILE: test/stubs-3013/njk/book.njk ================================================ --- layout: base pagination: data: books size: 1 alias: book permalink: /{{ book.shortname }}/ eleventyComputed: title: "{{ book.name }}" --- {{ title }} ================================================ FILE: test/stubs-3285/src/scripts/hello-world.js ================================================ export default function() { console.log('hello world'); }; ================================================ FILE: test/stubs-3356/.gitkeep ================================================ ================================================ FILE: test/stubs-337/data/xyz.json ================================================ { "hi": "bye" } ================================================ FILE: test/stubs-337/src/empty.md ================================================ ================================================ FILE: test/stubs-3807/Issue3807test.js ================================================ import test from "ava"; import fs from "node:fs"; import Eleventy from "../../src/Eleventy.js"; import { withResolvers } from "../../src/Util/PromiseUtil.js"; // This tests Eleventy Watch and the file system! test("#3807 Nunjucks cacheable should be reused when Nunjucks is the preprocessor language", async (t) => { let runs = [ { ...withResolvers(), input: `first{% block main %}{{ content | safe }}{% endblock %}`, expected: `firstHome

Index

`, }, { ...withResolvers(), input: `second{% block main %}{{ content | safe }}{% endblock %}`, expected: `secondHome

Index

`, }, { ...withResolvers(), input: `third{% block main %}{{ content | safe }}{% endblock %}`, expected: `thirdHome

Index

`, } ]; t.plan(runs.length + 1); // Restore original content const ORIGINAL_CONTENT = `{% block main %}{{ content | safe }}{% endblock %}`; fs.writeFileSync("test/stubs-3807/_layouts/base.html", ORIGINAL_CONTENT, "utf8"); let index = 0; let elev = new Eleventy("test/stubs-3807/", "test/stubs-3807/_site", { configPath: "test/stubs-3807/eleventy.config.js", config(eleventyConfig) { eleventyConfig.on("eleventy.afterwatch", () => { let {resolve} = runs[index]; index++; resolve(); }); } }); elev.disableLogger(); await elev.init(); await elev.watch(); // Control let content = fs.readFileSync("test/stubs-3807/_site/index.html", "utf8"); t.is(content, `Home

Index

`); // Stop after all runs are complete Promise.all(runs.map(entry => entry.promise)).then(async () => { await elev.stopWatch(); }); for(let run of runs) { fs.writeFileSync("test/stubs-3807/_layouts/base.html", run.input, "utf8"); await run.promise; let content = fs.readFileSync("test/stubs-3807/_site/index.html", "utf8"); t.is(content, run.expected); } fs.writeFileSync("test/stubs-3807/_layouts/base.html", ORIGINAL_CONTENT, "utf8"); fs.rmSync("test/stubs-3807/_site", { recursive: true }); }); ================================================ FILE: test/stubs-3807/_layouts/base.html ================================================ {% block main %}{{ content | safe }}{% endblock %} ================================================ FILE: test/stubs-3807/_layouts/home.html ================================================ {% extends "test/stubs-3807/_layouts/base.html" %}{% block main %}Home{{ content | trim | safe }}{% endblock %} ================================================ FILE: test/stubs-3807/eleventy.config.js ================================================ export default function(eleventyConfig) { eleventyConfig.setLayoutsDirectory("_layouts"); } export const config = { markdownTemplateEngine: "njk", htmlTemplateEngine: "njk", } ================================================ FILE: test/stubs-3807/index.md ================================================ --- layout: home.html --- Index ================================================ FILE: test/stubs-3810/_includes/promo.njk ================================================

Sign up for our {{ promoType }}!

================================================ FILE: test/stubs-3810/eleventy.config.js ================================================ import fs from 'fs'; import { RenderPlugin } from '../../src/Eleventy.js'; const { RenderManager } = RenderPlugin; export default function(eleventyConfig) { const rm = new RenderManager(); eleventyConfig.on('eleventy.config', cfg => { rm.templateConfig = cfg; }); eleventyConfig.addAsyncShortcode('promo', async function (promoType) { let content = fs.readFileSync('./test/stubs-3810/_includes/promo.njk').toString(); const fn = await rm.compile(content, 'njk'); return fn({ promoType }); }); } export const config = { markdownTemplateEngine: "njk", } ================================================ FILE: test/stubs-3810/index.md ================================================ {% promo "newsletter" %} ================================================ FILE: test/stubs-403/.eleventyignore ================================================ ./_includes/** ================================================ FILE: test/stubs-403/_includes/include.liquid ================================================ ================================================ FILE: test/stubs-403/template.liquid ================================================ ================================================ FILE: test/stubs-408-sass/_code.scss ================================================ code { padding: 0.25em; line-height: 0; } ================================================ FILE: test/stubs-408-sass/style.scss ================================================ --- layout: layout.njk --- @use "code.scss"; /* Comment */ ================================================ FILE: test/stubs-413/date-frontmatter.md ================================================ --- subtitle: New doc page date: 2019-03-13 20:18:42 +0000 tags: - docs --- ================================================ FILE: test/stubs-434/_includes/macros-filter.njk ================================================ {% macro label(text) %}{% endmacro %} ================================================ FILE: test/stubs-434/_includes/macros.njk ================================================ {% macro label(text) %}{% endmacro %} ================================================ FILE: test/stubs-475/_includes/layout.njk ================================================ {{ content | safe }} ================================================ FILE: test/stubs-475/transform-layout/transform-layout.njk ================================================ --- layout: layout.njk --- This is content. ================================================ FILE: test/stubs-630/_data/globalData0.cjs ================================================ module.exports = { datakey1: "datavalue0" }; ================================================ FILE: test/stubs-630/_data/globalData1.cjs ================================================ module.exports = { datakey1: "datavalue1" }; ================================================ FILE: test/stubs-630/_data/globalData2.json ================================================ { "datakey1": "datavalue2", "datakey2": "{{pkg.name}}--json" } ================================================ FILE: test/stubs-630/_data/globalData3.yaml ================================================ datakey1: datavalue3 datakey2: "{{pkg.name}}--yaml" ================================================ FILE: test/stubs-630/_data/globalData4.nosj ================================================ { "datakey1": "datavalue4", "datakey2": "{{pkg.name}}--nosj" } ================================================ FILE: test/stubs-630/_data/mergingGlobalData.cjs ================================================ module.exports = { datakey0: "cjs-value1", datakey1: "cjs-value1", cjskey: "cjs" }; ================================================ FILE: test/stubs-630/_data/mergingGlobalData.js ================================================ export default { datakey0: "js-value0", jskey: "js", }; ================================================ FILE: test/stubs-630/_data/mergingGlobalData.json ================================================ { "datakey0": "json-value1", "datakey1": "json-value1", "datakey2": "json-value2", "jsonkey": "json" } ================================================ FILE: test/stubs-630/_data/mergingGlobalData.nosj ================================================ { "datakey0": "nosj-value1", "datakey1": "nosj-value1", "datakey2": "nosj-value2", "datakey3": "nosj-value3", "datakey4": "nosj-value4", "nosjkey": "nosj" } ================================================ FILE: test/stubs-630/_data/mergingGlobalData.yaml ================================================ datakey0: "yaml-value1" datakey1: "yaml-value1" datakey2: "yaml-value2" datakey3: "yaml-value3" yamlkey: "yaml" ================================================ FILE: test/stubs-630/_data/subdir/globalDataSubdir.yaml ================================================ keyyaml: "yaml" ================================================ FILE: test/stubs-630/component-yaml/component.11tydata.cjs ================================================ module.exports = { jsKey1: "js1" }; ================================================ FILE: test/stubs-630/component-yaml/component.11tydata.json ================================================ { "jsonKey1": "json1" } ================================================ FILE: test/stubs-630/component-yaml/component.11tydata.nosj ================================================ { "nosjKey1": "nosj1" } ================================================ FILE: test/stubs-630/component-yaml/component.11tydata.yaml ================================================ yamlKey2: "yaml2" yamlKey3: "yaml3" jsonKey1: "overridden" jsKey1: "overridden" nosjKey1: "overridden" ================================================ FILE: test/stubs-630/component-yaml/component.json ================================================ { "jsonKey2": "json2", "jsKey1": "overridden", "yamlKey3": "overridden" } ================================================ FILE: test/stubs-630/component-yaml/component.njk ================================================ {{localkeyOverride}} ================================================ FILE: test/stubs-630/component-yaml/component.yaml ================================================ yamlKey1: "yaml1" yamlKey2: "overridden" jsonKey1: "overridden" jsonKey2: "overridden" jsKey1: "overridden" ================================================ FILE: test/stubs-670/content.njk ================================================ --- tags: - Cañon City --- ================================================ FILE: test/stubs-670/index.njk ================================================ {{ collections | length }},{% for key,item in collections %}{{ key }},{% endfor %} ================================================ FILE: test/stubs-919/test.11tydata.cjs ================================================ module.exports = function () { return { test: Math.random(), }; }; ================================================ FILE: test/stubs-919/test.njk ================================================ --- root: - one - two - three pagination: data: "root" size: 1 --- {{ test | log }} ================================================ FILE: test/stubs-919/test2.njk ================================================ ================================================ FILE: test/stubs-absolute/test.md ================================================ ================================================ FILE: test/stubs-addglobaldata/test.liquid ================================================ ================================================ FILE: test/stubs-addglobaldata-noop/test.txt ================================================ ================================================ FILE: test/stubs-autocopy/.gitkeep ================================================ ================================================ FILE: test/stubs-base/index.njk ================================================ --- permalink: /deep/ --- Home Test Test Test ================================================ FILE: test/stubs-base-case-sens/index.njk ================================================ --- permalink: /deep/ --- Home Test Test ================================================ FILE: test/stubs-circular-layout/_includes/layout-cycle-a.njk ================================================ --- layout: layouts/layout-cycle-b.njk --- ================================================ FILE: test/stubs-circular-layout/_includes/layout-cycle-b.njk ================================================ --- layout: layouts/layout-cycle-c.njk --- ================================================ FILE: test/stubs-circular-layout/_includes/layout-cycle-c.njk ================================================ --- layout: layouts/layout-cycle-a.njk --- ================================================ FILE: test/stubs-circular-layout/_includes/layout-cycle-self.njk ================================================ --- layout: layouts/layout-cycle-b.njk --- ================================================ FILE: test/stubs-computed-array/test.liquid ================================================ --- dynamicValue: "test" eleventyComputed: notArray: "{{ dynamicValue }}" array: - "static value" - "{{ dynamicValue }}" --- ================================================ FILE: test/stubs-computed-collections/collections.njk ================================================ ---js { eleventyComputed: { test: "hello", dogCollection: data => { return data.collections.dog; } } } --- Issue #1114 ================================================ FILE: test/stubs-computed-collections/dog.njk ================================================ --- tags: dog --- Hi from dog ================================================ FILE: test/stubs-computed-collections-filter/collections.njk ================================================ ---js { eleventyComputed: { test: "hello", dogCollection: data => { return data.collections.dog.filter(entry => true); } } } --- Issue #1114 ================================================ FILE: test/stubs-computed-collections-filter/dog.njk ================================================ --- tags: dog --- Hi from dog ================================================ FILE: test/stubs-computed-dirdata/dir/dir.11tydata.cjs ================================================ module.exports = { eleventyComputed: { webmentions: (data) => { return data.test; }, }, }; ================================================ FILE: test/stubs-computed-dirdata/dir/first.11ty.cjs ================================================ module.exports.data = { test: "first", }; module.exports.render = function (data) { return "first"; }; ================================================ FILE: test/stubs-computed-dirdata/dir/second.11ty.cjs ================================================ module.exports.data = { test: "second", }; module.exports.render = function (data) { return "second"; }; ================================================ FILE: test/stubs-computed-global/_data/eleventyComputed.cjs ================================================ module.exports = { eleventyNavigation: { key: data => { return "nested-first-global"; } }, image2: data => { return "second-global"; }, image3: data => { return "third-global"; } }; ================================================ FILE: test/stubs-computed-global/intermix.njk ================================================ ---js { eleventyComputed: { image: data => { return "first"; }, image2: data => { return "second"; } } } --- Issue #1043 ================================================ FILE: test/stubs-computed-pagination/child.11ty.cjs ================================================ module.exports.data = { eleventyComputed: { venues: (data) => { return data.collections.venue; }, }, }; ================================================ FILE: test/stubs-computed-pagination/paginated.njk ================================================ --- venues: - first - second pagination: data: venues size: 1 alias: venue addAllPagesToCollections: true permalink: "venues/{{ venue }}/" tags: venue eleventyComputed: title: "{{ venue }}" --- ================================================ FILE: test/stubs-computed-symbolparse/test.liquid ================================================ --- eleventyComputed: c: "{{ a | fail }}{{ b | fail }}" a: "a" b: "b" --- ================================================ FILE: test/stubs-computed-symbolparse/test.njk ================================================ --- eleventyComputed: c: "{{ a | fail }}{{ b | fail }}" a: "a" b: "b" --- ================================================ FILE: test/stubs-custom-extension/test.js1 ================================================

Paragraph

================================================ FILE: test/stubs-data-cascade/global-versus-layout/_data/cascade.cjs ================================================ module.exports = "from-global-data"; ================================================ FILE: test/stubs-data-cascade/global-versus-layout/_includes/base.njk ================================================ --- cascade: "from-layout-file" --- ================================================ FILE: test/stubs-data-cascade/global-versus-layout/test.njk ================================================ --- layout: "base.njk" --- ================================================ FILE: test/stubs-data-cascade/layout-data-files/_includes/base.njk ================================================ --- shared: layout --- ================================================ FILE: test/stubs-data-cascade/layout-data-files/test.11tydata.cjs ================================================ module.exports = { shared: "datafile" }; ================================================ FILE: test/stubs-data-cascade/layout-data-files/test.njk ================================================ --- layout: base.njk --- ================================================ FILE: test/stubs-data-cascade/layout-versus-dirdatafile/src/_includes/base.njk ================================================ --- cascade: "from-layout-file" --- ================================================ FILE: test/stubs-data-cascade/layout-versus-dirdatafile/src/src.11tydata.cjs ================================================ module.exports = { cascade: "dir-data-file", }; ================================================ FILE: test/stubs-data-cascade/layout-versus-dirdatafile/src/test.njk ================================================ --- layout: "base.njk" --- ================================================ FILE: test/stubs-data-cascade/layout-versus-tmpldatafile/_includes/base.njk ================================================ --- cascade: "from-layout-file" --- ================================================ FILE: test/stubs-data-cascade/layout-versus-tmpldatafile/test.11tydata.cjs ================================================ module.exports = { cascade: "template-data-file", }; ================================================ FILE: test/stubs-data-cascade/layout-versus-tmpldatafile/test.njk ================================================ --- layout: "base.njk" --- ================================================ FILE: test/stubs-data-esm/_data/commonjs.cjs ================================================ module.exports = "commonjs default"; ================================================ FILE: test/stubs-data-esm/_data/module.mjs ================================================ export const named = "es module named"; export default "es module default"; ================================================ FILE: test/stubs-dependency-tree/child.cjs ================================================ require("./grandchild.cjs"); ================================================ FILE: test/stubs-dependency-tree/grandchild.cjs ================================================ require("kleur"); ================================================ FILE: test/stubs-dependency-tree/index.cjs ================================================ require("@11ty/lodash-custom"); require("./child.cjs"); ================================================ FILE: test/stubs-empty/.gitkeep ================================================ ================================================ FILE: test/stubs-empty-json-data/_data/empty.json ================================================ ================================================ FILE: test/stubs-fancyjs/test.11ty.tsx ================================================ import React from "react"; function render(data: object) { return
hello world 1
; } export { render } ================================================ FILE: test/stubs-fancyjs/test.mdx ================================================ export function Thing() { return <>World!!!! } # Hello, ================================================ FILE: test/stubs-freeze/eleventy/_data/eleventy.js ================================================ export default { "key": "value", // not allowed }; ================================================ FILE: test/stubs-freeze/page/_data/page.js ================================================ export default { "key": "value", // allowed "url": "lksjdklfjlskdjf", // not allowed }; ================================================ FILE: test/stubs-global-data-config-api/empty.txt ================================================ ================================================ FILE: test/stubs-global-data-config-api-nested/_data/deep.cjs ================================================ module.exports = { existing: true, }; ================================================ FILE: test/stubs-i18n/en/index.liquid ================================================ {{ "/" | locale_url }} {{ "/en-us/" | locale_url }} {{ "/es/" | locale_url }} {{ "/" | locale_url: "en-us" }} {{ "/non-lang-file/" | locale_url }} {{ page.url | locale_links | json }} {{ "" | locale_links | json }} {{ page.lang }} ================================================ FILE: test/stubs-i18n/en-us/index.11ty.cjs ================================================ module.exports = function (data) { return `${this.locale_url("/")} ${this.locale_url("/en-us/")} ${this.locale_url("/es/")} ${this.locale_url("/", "es")} ${this.locale_url("/non-lang-file/")} ${JSON.stringify(this.locale_links(data.page.url).sort())} ${JSON.stringify(this.locale_links().sort())} ${data.page.lang}`; }; ================================================ FILE: test/stubs-i18n/es/index.njk ================================================ {{ "/" | locale_url }} {{ "/en-us/" | locale_url }} {{ "/es/" | locale_url }} {{ "/" | locale_url("en-us") }} {{ "/non-lang-file/" | locale_url }} {{ page.url | locale_links | dump | safe }} {{ "" | locale_links | dump | safe }} {{ page.lang }} ================================================ FILE: test/stubs-i18n/non-lang-file.njk ================================================ {{ "/" | locale_url }} {{ "/" | locale_url("en-us") }} {{ "/non-lang-file/" | locale_url }} {{ page.url | locale_links | dump | safe }} {{ "" | locale_links | dump | safe }} {{ page.lang }} ================================================ FILE: test/stubs-img-transform/ignored.md ================================================ it’s a possum ================================================ FILE: test/stubs-img-transform/missing-alt.md ================================================ ================================================ FILE: test/stubs-img-transform/multiple.md ================================================ it’s a possum it’s a possum ================================================ FILE: test/stubs-img-transform/single.md ================================================ it’s a possum ================================================ FILE: test/stubs-incremental/layout-chain/_includes/base.njk ================================================ --- layout: parent.njk --- ================================================ FILE: test/stubs-incremental/layout-chain/_includes/parent.njk ================================================ ================================================ FILE: test/stubs-incremental/layout-chain/test.njk ================================================ --- layout: base.njk --- ================================================ FILE: test/stubs-layout-cache/_includes/layout.liquid ================================================ ================================================ FILE: test/stubs-layout-cache/_includes/layout.njk ================================================ ================================================ FILE: test/stubs-layout-cache/test.liquid ================================================ --- layout: "layout.liquid" --- Content ================================================ FILE: test/stubs-layout-cache/test.njk ================================================ --- layout: "layout.njk" --- Content ================================================ FILE: test/stubs-layouts-event/_includes/first.liquid ================================================ --- layout: second --- {{ content }} ================================================ FILE: test/stubs-layouts-event/_includes/second.liquid ================================================ --- layout: third.liquid --- {{ content }} ================================================ FILE: test/stubs-layouts-event/_includes/third.liquid ================================================ {{ content }} ================================================ FILE: test/stubs-layouts-event/page.md ================================================ --- layout: first --- ================================================ FILE: test/stubs-njk-async/_includes/loop.njk ================================================ included_{{item}}-{% genericshortcode item %} ================================================ FILE: test/stubs-pagination-computed-quotes/post.liquid ================================================ --- tags: posts --- No ================================================ FILE: test/stubs-pagination-computed-quotes/test.liquid ================================================ --- pagination: data: "collections.posts" size: 1 quotes: - The person that shared this is awesome eleventyComputed: quote: "{{ quotes | selectRandomFromArray }}" --- {{ quote }} ================================================ FILE: test/stubs-pagination-computed-quotes-njk/post.njk ================================================ --- tags: posts --- No ================================================ FILE: test/stubs-pagination-computed-quotes-njk/test.njk ================================================ --- pagination: data: "collections.posts" size: 1 quotes: - The person that shared this is awesome eleventyComputed: quote: "{{ quotes | selectRandomFromArray }}" --- {{ quote }} ================================================ FILE: test/stubs-pathtourl/css.njk ================================================ --- permalink: output.css --- ================================================ FILE: test/stubs-pathtourl/filter.njk ================================================ Home Test Anchor Anchor Anchor Search Search ================================================ FILE: test/stubs-pathtourl/tmpl.njk ================================================ ================================================ FILE: test/stubs-pathtourl/transform.njk ================================================ Home Test Anchor Anchor Anchor Search Search ================================================ FILE: test/stubs-render-plugin/11tyjs-file-override.njk ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderFile "./test/stubs-render-plugin/_includes/include-js.txt", argData, "11ty.js" %} ================================================ FILE: test/stubs-render-plugin/11tyjs-file.njk ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderFile "./test/stubs-render-plugin/_includes/include.11ty.cjs", argData %} ================================================ FILE: test/stubs-render-plugin/11tyjs.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderTemplate "11ty.js" argData %} module.exports = "TESTING"; {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/_includes/frontmatter.liquid ================================================ --- test: frontmatterString --- {{ test }} ================================================ FILE: test/stubs-render-plugin/_includes/include-js.txt ================================================ module.exports = "TESTING"; ================================================ FILE: test/stubs-render-plugin/_includes/include.11ty.cjs ================================================ module.exports = "TESTING"; ================================================ FILE: test/stubs-render-plugin/_includes/include.liquid ================================================ TESTING {% renderTemplate "liquid" %} TESTING IN LIQUID {% assign liquidassign = 999 %} * {{ liquidassign }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/_includes/include.njk ================================================ TESTING {% renderTemplate "liquid" %} TESTING IN LIQUID {% assign liquidassign = 999 %} * {{ liquidassign }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/bad-data.njk ================================================ {% renderTemplate "liquid", "string" %} {{ _ }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/capture-liquid.njk ================================================ --- argData: num: 2 --- {% setAsync "liquidOutput" %} {% renderTemplate "liquid", argData %} {% assign liquidVar = num | times: 2 %} {{ liquidVar }} {% endrenderTemplate %} {% endsetAsync %} {{ liquidOutput }} ================================================ FILE: test/stubs-render-plugin/capture-njk.liquid ================================================ --- argData: num: 2 --- {% capture nunjucksOutput %} {% renderTemplate "njk", argData %} {% set njkVar = num * 2 %} {{ njkVar }} {% endrenderTemplate %} {% endcapture %} {{ nunjucksOutput }} ================================================ FILE: test/stubs-render-plugin/data-no-templatelang.liquid ================================================ --- argData: name: Bruno --- {% renderTemplate argData %} # Hello {{ name }} * Testing {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/false.liquid ================================================ {% renderTemplate %} {% assign name = "Bruno" %} # Hello {{ name }} * Testing {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-direct.njk ================================================ {%- set nunjucksVar = 69 -%} {{ hi }} {{ nunjucksVar }} {{ "who" | testing }} {% renderTemplate "liquid", argData %} {% assign liquidVar = 138 %} * {{ hi }} test test {{ bye }} {{ liquidVar }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-eleventy.njk ================================================ --- --- {% renderTemplate "liquid" %} {{ eleventy.version }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-global.njk ================================================ --- hi: globalHi --- {% renderTemplate "liquid" %} {{ hi }}123 {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-md.11ty.cjs ================================================ class Tmpl { data() { return { argData: { hi: "javascriptHi", bye: "javascriptBye", }, }; } async render(data) { return this.renderTemplate( ` # Markdown {% assign t1 = 2 %} * {{ t1 }} `, "liquid,md", data.argData ); } } module.exports = Tmpl; ================================================ FILE: test/stubs-render-plugin/liquid-md.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderTemplate "liquid,md" argData %} # Hello {{ hi }} * Testing {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-page.liquid ================================================ {% renderTemplate "liquid" %} {{ page.url }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid-page.njk ================================================ {% renderTemplate "liquid" %} {{ page.url }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/liquid.njk ================================================ --- # Nunjucks hi: nunjucksHi argData: hi: liquidHi bye: liquidBye --- {%- set nunjucksVar = 69 -%} {{ hi }} {{ nunjucksVar }} {% renderTemplate "liquid", argData %} {% assign liquidVar = 138 %} * {{ hi }} test test {{ bye }} {{ liquidVar }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/md.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderTemplate "md" argData %} # Hello {{ hi }} * Testing {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/njk-eleventy.liquid ================================================ --- --- {% renderTemplate "njk" %} {{ eleventy.version }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/njk-file-not-exist.liquid ================================================ {% renderFile "./test/stubs-render-plugin/THIS_DOES_NOT_EXIST.njk" %} ================================================ FILE: test/stubs-render-plugin/njk-file.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderFile "./test/stubs-render-plugin/_includes/include.njk" argData %} ================================================ FILE: test/stubs-render-plugin/njk-file.njk ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderFile "./test/stubs-render-plugin/_includes/include.liquid", argData %} ================================================ FILE: test/stubs-render-plugin/njk-page.liquid ================================================ {% renderTemplate "njk" %} {{ page.url }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/nunjucks-frontmatter.njk ================================================ --- hi: "{% test %}" --- {{ hi | renderContent("njk") }} {# {% renderTemplate "njk", hi %}{{ _ }}{% endrenderTemplate %} #} ================================================ FILE: test/stubs-render-plugin/nunjucks-global.liquid ================================================ --- hi: globalHi --- {% renderTemplate "njk" %} {{ hi }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/nunjucks.11ty.cjs ================================================ class Tmpl { data() { return { argData: { hi: "javascriptHi", bye: "javascriptBye", }, }; } async render(data) { return this.renderTemplate( ` * {{ hi | reverse }} `, "njk", data.argData ); } } module.exports = Tmpl; ================================================ FILE: test/stubs-render-plugin/nunjucks.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderTemplate "njk" argData %} {% set bye = "abldskjfl" %} * {{ hi | reverse }} * {{ bye | reverse }} {% endrenderTemplate %} ================================================ FILE: test/stubs-render-plugin/using-frontmatter.liquid ================================================ {% renderFile "./test/stubs-render-plugin/_includes/frontmatter.liquid" %} ================================================ FILE: test/stubs-render-plugin/vue.liquid ================================================ --- hi: value argData: hi: liquidHi bye: liquidBye --- {% renderTemplate "vue" argData %}
HELLO WE ARE VUEING

{% endrenderTemplate %} ================================================ FILE: test/stubs-virtual/.gitkeep ================================================ ================================================ FILE: test/stubs-virtual/eleventy.config.js ================================================ // generic config file export default function(eleventyConfig) {}; ================================================ FILE: test/stubs-virtual-nowrite/.gitkeep ================================================ ================================================ FILE: test/views/.gitkeep ================================================ ================================================ FILE: test_node/3824/3824-test.js ================================================ // This test file is using Node’s test runner because `tsx` doesn’t work with worker threads (used by avajs) // See https://github.com/privatenumber/tsx/issues/354 // See https://github.com/nodejs/node/issues/47747 import test from "node:test"; import fs from "node:fs"; import assert from "node:assert"; import Eleventy from "../../src/Eleventy.js"; import { withResolvers } from "../../src/Util/PromiseUtil.js"; // This tests Eleventy Watch and the file system! function getInputContent(str = "") { return `import { Page } from "./ViewProps.js"; export type HeadProps = { page: Page }; export function Head(props: HeadProps): JSX.Element { return My test page${str} ; }`; } function getOutputContent(str = "") { return `My test page${str}

Hello World

`; } test( "#3824 TSX updates during watch", { timeout: 10000, }, async () => { let comparisonStrings = ["first", "second"]; let runs = comparisonStrings.map((str) => { return { ...withResolvers(), input: getInputContent(str), expected: getOutputContent(str), }; }); // Restore original content const ROOT_DIR = "./test_node/3824/"; const OUTPUT_DIR = ROOT_DIR + "_site/"; const FILE_CHANGING = ROOT_DIR + "_includes/head.tsx"; const OUTPUT_FILE = OUTPUT_DIR + "index.html"; fs.writeFileSync(FILE_CHANGING, getInputContent(), "utf8"); let index = 0; let elev = new Eleventy(ROOT_DIR, OUTPUT_DIR, { configPath: ROOT_DIR + "eleventy.config.js", config(eleventyConfig) { eleventyConfig.on("eleventy.afterwatch", () => { let { resolve } = runs[index]; index++; resolve(); }); }, }); elev.disableLogger(); await elev.init(); await elev.watch(); // Control let content = fs.readFileSync(OUTPUT_FILE, "utf8"); assert.equal(content, getOutputContent()); // Stop after all runs are complete Promise.all(runs.map((entry) => entry.promise)).then(async () => { await elev.stopWatch(); }); for (let run of runs) { // Windows/Ubuntu needed this for Chokidar reasons await new Promise((resolve) => setTimeout(resolve, 200)); fs.writeFileSync(FILE_CHANGING, run.input, "utf8"); await run.promise; let content = fs.readFileSync(OUTPUT_FILE, "utf8"); assert.equal(content, run.expected); } fs.writeFileSync(FILE_CHANGING, getInputContent(), "utf8"); fs.rmSync(OUTPUT_DIR, { recursive: true }); }, ); ================================================ FILE: test_node/3824/_includes/head.tsx ================================================ import { Page } from "./ViewProps.js"; export type HeadProps = { page: Page }; export function Head(props: HeadProps): JSX.Element { return My test page ; } ================================================ FILE: test_node/3824/_includes/view-props.tsx ================================================ ================================================ FILE: test_node/3824/eleventy.config.js ================================================ import { register } from "tsx/esm/api"; import { jsxToString } from "jsx-async-runtime"; // import { renderToStaticMarkup } from "react-dom/server"; export default async function (eleventyConfig) { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: async function (inputContent, inputPath) { this.addDependencies(inputPath, ["./test_node/3824/_includes/head.tsx"]); return async function (data) { let content = await this.defaultRenderer(data); return jsxToString(content); // return renderToStaticMarkup(content); }; }, }); eleventyConfig.addTemplateFormats(["11ty.jsx", "11ty.tsx"]); let unregister; eleventyConfig.on("eleventy.before", () => { unregister = register({ // custom tsconfig tsconfig: "test_node/3824/tsconfig-3824.json", }); }); eleventyConfig.on("eleventy.after", () => { unregister(); }); } ================================================ FILE: test_node/3824/index.11ty.tsx ================================================ import { Head } from "./_includes/head.tsx"; import { Page, ViewProps } from "./_includes/viewprops.tsx"; export type IndexProps = { children?: JSX.Element; page: Page }; export function Index(props: IndexProps): JSX.Element { return

Hello World

; } export function render(props: ViewProps): JSX.Element { return ; } ================================================ FILE: test_node/3824/tsconfig-3824.json ================================================ { "compilerOptions": { "target": "es2016", "jsx": "react-jsx", "jsxImportSource": "jsx-async-runtime", "module": "NodeNext", "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "exclude": [ "node_modules", "_site" ] } ================================================ FILE: test_node/3824-incremental/3824-incremental-test.js ================================================ // This test file is using Node’s test runner because `tsx` doesn’t work with worker threads (used by avajs) // See https://github.com/privatenumber/tsx/issues/354 // See https://github.com/nodejs/node/issues/47747 import test from "node:test"; import fs from "node:fs"; import assert from "node:assert"; import Eleventy from "../../src/Eleventy.js"; import { withResolvers } from "../../src/Util/PromiseUtil.js"; // This tests Eleventy Watch and the file system! function getInputContent(str = "") { return `import { Page } from "./ViewProps.js"; export type HeadProps = { page: Page }; export function Head(props: HeadProps): JSX.Element { return My test page${str} ; }`; } function getOutputContent(str = "") { return `My test page${str}

Hello World

`; } test( "#3824 TSX updates during watch (incremental)", { timeout: 10000, }, async () => { let comparisonStrings = ["first", "second"]; let runs = comparisonStrings.map((str) => { return { ...withResolvers(), input: getInputContent(str), expected: getOutputContent(str), }; }); // Restore original content const ROOT_DIR = "./test_node/3824-incremental/"; const OUTPUT_DIR = ROOT_DIR + "_site/"; const FILE_CHANGING = ROOT_DIR + "_includes/head.tsx"; const OUTPUT_FILE = OUTPUT_DIR + "index.html"; fs.writeFileSync(FILE_CHANGING, getInputContent(), "utf8"); let index = 0; let elev = new Eleventy(ROOT_DIR, OUTPUT_DIR, { configPath: ROOT_DIR + "eleventy.config.js", config(eleventyConfig) { eleventyConfig.on("eleventy.afterwatch", () => { let { resolve } = runs[index]; index++; resolve(); }); }, }); // Same as 3824-test.js except for this line elev.setIncrementalBuild(true); elev.disableLogger(); await elev.init(); await elev.watch(); // Control let content = fs.readFileSync(OUTPUT_FILE, "utf8"); assert.equal(content, getOutputContent()); // Stop after all runs are complete Promise.all(runs.map((entry) => entry.promise)).then(async () => { await elev.stopWatch(); }); for (let run of runs) { // Windows/Ubuntu needed this for Chokidar reasons await new Promise((resolve) => setTimeout(resolve, 200)); fs.writeFileSync(FILE_CHANGING, run.input, "utf8"); await run.promise; let content = fs.readFileSync(OUTPUT_FILE, "utf8"); assert.equal(content, run.expected); } fs.writeFileSync(FILE_CHANGING, getInputContent(), "utf8"); fs.rmSync(OUTPUT_DIR, { recursive: true }); }, ); ================================================ FILE: test_node/3824-incremental/_includes/head.tsx ================================================ import { Page } from "./ViewProps.js"; export type HeadProps = { page: Page }; export function Head(props: HeadProps): JSX.Element { return My test page ; } ================================================ FILE: test_node/3824-incremental/_includes/view-props.tsx ================================================ ================================================ FILE: test_node/3824-incremental/eleventy.config.js ================================================ import { register } from "tsx/esm/api"; import { jsxToString } from "jsx-async-runtime"; // import { renderToStaticMarkup } from "react-dom/server"; export default async function (eleventyConfig) { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: async function (inputContent, inputPath) { this.addDependencies(inputPath, ["./test_node/3824-incremental/_includes/head.tsx"]); return async function (data) { let content = await this.defaultRenderer(data); return jsxToString(content); // return renderToStaticMarkup(content); }; }, }); eleventyConfig.addTemplateFormats(["11ty.jsx", "11ty.tsx"]); let unregister; eleventyConfig.on("eleventy.before", () => { unregister = register({ // custom tsconfig tsconfig: "test_node/3824-incremental/tsconfig-3824.json", }); }); eleventyConfig.on("eleventy.after", () => { unregister(); }); } ================================================ FILE: test_node/3824-incremental/index.11ty.tsx ================================================ import { Head } from "./_includes/head.tsx"; import { Page, ViewProps } from "./_includes/viewprops.tsx"; export type IndexProps = { children?: JSX.Element; page: Page }; export function Index(props: IndexProps): JSX.Element { return

Hello World

; } export function render(props: ViewProps): JSX.Element { return ; } ================================================ FILE: test_node/3824-incremental/tsconfig-3824.json ================================================ { "compilerOptions": { "target": "es2016", "jsx": "react-jsx", "jsxImportSource": "jsx-async-runtime", "module": "NodeNext", "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "exclude": [ "node_modules", "_site" ] } ================================================ FILE: test_node/JsxTest.js ================================================ // This test file is using Node’s test runner because `tsx` doesn’t work with worker threads (used by avajs) // See https://github.com/privatenumber/tsx/issues/354 // See https://github.com/nodejs/node/issues/47747 import test from "node:test"; import assert from "node:assert"; import { renderToStaticMarkup } from "react-dom/server"; // Typically import 'tsx/esm'; but we use register method to work with test isolation // import { register } from 'tsx/esm/api' import "tsx/esm"; // import 'tsimp'; import Eleventy from "../src/Eleventy.js"; test("Eleventy with JSX", async () => { let elev = new Eleventy("./test/stubs-fancyjs/test.11ty.tsx", undefined, { config: (eleventyConfig) => { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: function () { return async function (data) { let content = await this.defaultRenderer(data); return renderToStaticMarkup(content); }; }, }); }, }); elev.setFormats("11ty.tsx"); let results = await elev.toJSON(); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].content, `
hello world 1
`); }); test("Eleventy no formats", async () => { let elev = new Eleventy("./test/stubs-fancyjs/", undefined, { config: (eleventyConfig) => { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: function () { return async function (data) { let content = await this.defaultRenderer(data); return renderToStaticMarkup(content); }; }, }); }, }); // elev.setFormats("") let results = await elev.toJSON(); assert.strictEqual(results.length, 0); }); test("Eleventy JSX --formats=11ty.tsx", async () => { let elev = new Eleventy("./test/stubs-fancyjs/", undefined, { config: (eleventyConfig) => { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: function () { return async function (data) { let content = await this.defaultRenderer(data); return renderToStaticMarkup(content); }; }, }); }, }); elev.setFormats("11ty.tsx"); let results = await elev.toJSON(); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].content, `
hello world 1
`); }); test("Eleventy JSX --formats=tsx", async () => { let elev = new Eleventy("./test/stubs-fancyjs/", undefined, { config: (eleventyConfig) => { eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], { key: "11ty.js", compile: function () { return async function (data) { let content = await this.defaultRenderer(data); return renderToStaticMarkup(content); }; }, }); }, }); elev.setFormats("tsx"); // should not pick up 11ty.tsx let results = await elev.toJSON(); assert.strictEqual(results.length, 0); // Should have no results!! }); ================================================ FILE: test_node/MdxTest.js ================================================ // This test file is using Node’s test runner because `tsx` doesn’t work with worker threads (used by avajs) // See https://github.com/privatenumber/tsx/issues/354 // See https://github.com/nodejs/node/issues/47747 import test from "node:test"; import assert from "node:assert"; import module from "node:module"; import { renderToStaticMarkup } from "react-dom/server"; import Eleventy from "../src/Eleventy.js"; if ("register" in module) { module.register("@mdx-js/node-loader", import.meta.url); } test("Eleventy with MDX", async () => { let elev = new Eleventy("./test/stubs-fancyjs/test.mdx", undefined, { config: (eleventyConfig) => { eleventyConfig.addExtension("mdx", { key: "11ty.js", compile: () => { return async function (data) { let content = await this.defaultRenderer(data); return renderToStaticMarkup(content); }; }, }); }, }); elev.disableLogger(); elev.setFormats("mdx"); let results = await elev.toJSON(); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].content, `

Hello, World!!!!

`); }); ================================================ FILE: test_node/README.md ================================================ # test_node Unit Tests This folder is for tests using the [official Node Test Runner](https://nodejs.org/api/test.html). It was originally introduced to workaround issues with `tsx` and `@mdx-js/node-loader` using worker threads (not supported by the existing test runner, [ava](https://github.com/avajs/ava)). We’re using this instead of `--no-worker-threads` with a separate `ava` run. ================================================ FILE: test_node/tests.js ================================================ import "./JsxTest.js"; import "./MdxTest.js"; import "./3824/3824-test.js"; import "./3824-incremental/3824-incremental-test.js"; ================================================ FILE: tsconfig.json ================================================ { "include": [ // "src/Eleventy.js", "src/UserConfig.js", "src/Util/ConsoleLogger.js", ], "exclude": [], "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": ["ES2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "Node16", /* Specify what module code is generated. */ // "rootDir": "./src/", /* Specify the root folder within your source files. */ "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // WARNING: this causes missing `node` types even with "types": ["node"] // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ "maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outDir": "./types/", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./types/", /* Specify the output directory for generated declaration files. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipLibCheck": true /* Skip type checking all .d.ts files. */ } }