11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
16.68k stars 484 forks source link

Typings via 11ty.ts #3296

Open panoply opened 2 months ago

panoply commented 2 months ago

Is your feature request related to a problem? Please describe.

TypeScript support, more specifically declarations within .eleventy.js configuration files seems to be a rather problematic for folks. The current approaches wherein using jsdoc annotations does not really suffice, nor does it solve plugin types. I've touched on this previously in issues, but I am yet to see any actionable steps in this area.

Describe the solution you'd like

I've gone ahead and exposed support for Eleventy in an isolated manner via 11ty.ts and this brings type support for the available API, with both JSDoc descriptions and linked documentation references available in declarations. In addition, plugins which expose types are also supported using an auto-typed tactic.

I believe this will assist developers in their usage with 11ty and solves all issues pertaining to type related support. Given that auto-typed plugins is made available, this also allows for great support in th eco. The implementation is quite simple, developers can just consume and expose using a defineConfig() export.

For Example:

const { defineConfig } = require("11ty.ts");

module.exports = defineConfig (function(eleventyConfig) {

  eleventyConfig.addPlugin()

  return {};

})

Preview

https://github.com/11ty/eleventy/assets/7041324/569190b3-cd3a-4100-b4d4-fd7e0c910623

Describe alternatives you've considered

No response

Additional context

If the overall TypeScript discussions and issues around typings are not something planned or have been concluded upon, it would be nice that developers can more easily find this solution, from both a maintenance side and also availability side via the 11ty official documentation.

Lastly, the reason this is not made available to DefinitelyTyped is because their is JS required, given the defineConfig export. It is not possible to bridge fluid support without applying a wrapper.

uncenter commented 1 month ago

I've been using 11ty.ts for a bit now and it has been so helpful! I think the defineConfig approach would be a great change, and it isn't unfamiliar to developers these days - projects like Cypress, Playright, Solid, Vitest, Astro, Drizzle, Nuxt, Rsbuild, Vite, etc all use the same defineConfig or similar helper.

zachleat commented 17 hours ago

For the record I do think the pattern you’ve shown here is better than what we have now!

I’d love to simplify this on the docs: https://www.11ty.dev/docs/config/#type-definitions

How can we best (and in the most simple way) make something like this happen?

import { defineConfig } from "@11ty/eleventy";
module.exports = defineConfig((eleventyConfig) {

});

Is it possible using TypeScript JSDoc comments or do we need .d.ts as noted here https://github.com/11ty/eleventy/issues/3097#issuecomment-2256860341

I’m also happy to link to the 11ty.ts from our TypeScript docs too, feel free to PR it here: https://www.11ty.dev/docs/languages/typescript/#using-a-typescript-configuration-file

panoply commented 15 hours ago

Ideally, you'd want use a declaration opposed to relying upon jsdoc annotation typings. The reason for this is because there are restrictions apparent and underlying limitations with jsdoc type enhancements. Typically you'll find that jsdoc annotation typings are leveraged for internal reference whereas .d.ts are intended for type acquisition (IIRC jsdoc typings involve additional processing by the TypeScript language server). One of the main benefits of 11ty.ts is that the exposed declaration supports plugin types automatically whenever a community plugin has exposed them, something which is great for plugins that require detailed options.

The pattern overall allows for things to be handled in isolation, alleviating potential headaches while keeping things separate and respecting the beautiful JS purity of the Eleventy source code. Appropriating it directly within the module, just as your above code snippet suggests can be done using a named export, as long as the declaration matches and is exposed within types key of package.json.

In regards to https://github.com/11ty/eleventy/issues/3097#issuecomment-2256860341, I'm not sure I totally understand why are files be processed using the TypeScript compiler here, is this to generate .d.ts from jsdoc annotations?


Bipartite relating to .eleventy.ts processing

Though a little unrelated to this issue specifically, but still in the realm of TypeScript, I am interested in the tactic for processing a .ts configuration file (e.g, .eleventy.ts). I have seen some discussions around this in the issues and IIRC the recommendation was to execute via ts-node. The inception of esbuild has given us a lot of opportunity to normalise logic, specifically CJS/ESM pain-points but also TypeScript. I am curious if there has been any consideration on supporting this by default?

Transforming .ts eleventy configuration file into esm/cjs at runtime using esbuild would address the typical nuances developers encounter in more complex .eleventy files. It might sound extraneous, but it's a relatively inexpensive operation to process config files using esbuild at execution runtime (excluding @11ty/eleventy), with large files typically concluding in less than 100ms. Processing using esbuild here and then executing (i.e; build, watch etc) upon a temporary transformed .eleventy.js would allow CJS, ESM and TypeScript files, including any additional modules (plugins) to be gracefully handled. This is somewhat the same tactic employed by other bundlers that support varying configuration file types.

uncenter commented 15 hours ago

On the topic of a TypeScript configuration file, I've personally used and seen widespread adoption of https://github.com/unjs/jiti. Seems like a good place to start as a loader for configuration files! Maybe best to create a separate issue for this topic? Not seeing any existing ones.

panoply commented 14 hours ago

Haven't seen unjs. Very basic approach, excluding the analysis for determining whether executing in a CJS/ESM environment would be something like the below (untested and excluding varying gotchas) but general pattern would be:

import { build } from 'esbuild';
import path from 'node:path';
import { writeFileSync, unlink } from 'node:fs/promise'
const cwd = process.cwd();

async function extract (result) {
    let run;
    const { text } = result.outputFiles[0];
    const outfile = options.filepath;
    writeFileSync(outfile, text, 'utf8'); // write file
    try {
      // example sake, more logic required but the general gist
      run = format === 'esm' ? import(outfile) : require(outfile);
    } finally { await unlink(outfile);   /* Remove temp outfile after executed*/ }
    return run;
 };

async function configRequire(format) {
  const context = await build({
    // config file name will need to be obtained, using .ts for example
    entryPoints: [ path.join(cwd, '.eleventy.ts') ], 
    absWorkingDir: cwd,
    outfile: 'out.js',
    format, // CJS or ESM depending on ENV (pre-determine)
    write: false,
    platform: 'node',
    sourcemap: 'inline',
    bundle: true,
    metafile: true,
    plugins: [ /* additional plug would be needed for import.meta.url  injects*/ ]
  });
  return extract(context); // the default export function is resolved
}

configRequire('cjs').then(fn => { /* fn() will hold the module.exports or default export */  })