nuxt / rfcs

RFCs for changes to Nuxt.js
99 stars 2 forks source link

Improve Config Package #16

Closed pi0 closed 2 years ago

pi0 commented 5 years ago

Objectives:

Related: https://github.com/nuxt/nuxt.js/issues/3985

manniL commented 5 years ago

nuxt.config validation (Both type and keys) and IDE autocomplete

Might be hard (for now) because of modules which can freely choose their used keys. We can validate the core keys without problems though.

pi0 commented 5 years ago

@manniL Exactly. At least the top level object cannot be strictly validated. We might add a special __validate__ key to the options that are validatable. If we use a well-defined object for schema definition we can magically use it for automated .d.ts generation too.

Example:

{ 
 __validate__: {
       _strict: false, // <-- Allow modules freely adding new keys for BW compat
       build: { type: Object }
 },
 build: {
     __validate__: {
        _strict: true, // <-- Indicates no extrra options are accepted
        name: { type: String, required: false },
        old: { type: String, deprecated: true }
    }
 }
}

We can also think about using a 3rd party schema validator that is able to generate types too. Like joi + joi-ts-generator

atinux commented 5 years ago

Supporting config/ directory and importing the files and merging them with nuxt.config.js (that could be deprecated in the future).

Example:

config/
  build/
    babel.js
  pages.js
  app.js

This logic could use directly glob right inside @nuxt/config. So writing configuration could also feel like writing pages <3

kevinmarrec commented 5 years ago

@Atinux Nice, I really liked how @manniL went with his own website with a config folder : https://github.com/manniL/lichter.io/tree/master/config. It seems to go in the same way of your config directory support

wagerfield commented 5 years ago

I would be wary of using a directory called config since there is already a popular package called config that reads JSON files from this directory at the root of your project:

https://www.npmjs.com/package/config

I've used the config package a number of times in Nuxt projects and it might get a little muddled if there are Nuxt related config JS files alongside static JSON config files.

Perhaps nuxt is a better default? Appreciate that Nuxt generates a .nuxt dir for dev and build, but nuxt without the . could be for config. Funnily enough, this is the convention that I have adopted for a recent project:

// nuxt.config.js
import generate from "./nuxt/generate"
import hooks from "./nuxt/hooks"
import head from "./nuxt/head"
import env from "./nuxt/env"

export default {
  srcDir: "src",
  generate,
  hooks,
  head,
  env
}
manniL commented 5 years ago

@wagerfield If we implement that feature, the directory name will be changeable ☺️ But thanks for the heads up!

manniL commented 5 years ago

Another thing that might come in handy:

A context object which can/will be passed to each sub config and can be set by the user. This might help to reduce async calls to APIs during build (e.g. when you want to define your routes for the sitemap, the blog feed and generate).

kevinmarrec commented 5 years ago

@manniL context could be of type NuxtConfiguration in TypeScript environments: https://github.com/kevinmarrec/nuxt.js/blob/nuxt-configuration-types/packages/config/types/index.d.ts#L18

EDIT : might have misunderstood context proposal

manniL commented 5 years ago

@kevinmarrec I thought about sth. like an argument that every sub-function could take (if it's written as function)

so the subconfigs would be sth. like export default (context) => { /* Do sth. here */ }

I saw that @TimKor (hope that was the right handle πŸ™ˆ) applied such a pattern:

const context = require('./shared/utils/context');

context.print();

module.exports = {

    server: require('./config/server.js')(context),

    env: require('./config/env.js')(context),
    /*
     ** Headers of the page
     */
    head: require('./config/head.js')(context),

    // Sitemap settings:
    sitemap: require('./config/sitemap.js')(context)
}
kevinmarrec commented 5 years ago

Oh yes right ! I'm now thinking of a new behavior for subconfigs πŸ€”

What about

// config/build.js
export default function (config, context) {
  config.build = {
    property: context.value
  }

  return config
}

The idea is to read every files in config folder and merge the values to build the final configuration file. People could preprend their filenames with numbers in cases they need to override same configuration properties in certain order in different files (rare case but could happen)

EDIT : It would work likely like Webpack extend config

wagerfield commented 5 years ago

Further to the discussion in #10 @pi0 suggested adding a presets field to the Nuxt config that would work in the same way as Babel. As an alternative, I prefer ESLint's extends field and behaviour for this use case since it supports a single string and an array of strings. I also think extends is more semantically correct πŸ€·β€β™‚οΈ

The value assigned to this field could either be a string or an array of strings that resolve to files exporting a Nuxt config:

// nuxt.config.js
export default {
  // String value resolving to "some-nuxt-preset" module exporting a Nuxt config
  extends: "some-nuxt-preset"

  // Can also be an array of strings, merging these presets in order
  extends: [
    "some-nuxt-preset",
    "another-nuxt-package/config"
  ]
}
pi0 commented 5 years ago

@wagerfield I like the extends more :+1:

Timkor commented 5 years ago

@manniL Almost right . :) I like to build the configuration using functions that accept a context parameter. Context is in my cases just based on some environment variables.

Which could also have been just the env key of the configuration. If Nuxt keeps track of a (configurable) order in which to load the configuration files, it would be possible to export a function like this:

~/config/env.js <-- first one by default


export default (config) => {
    return {
        locale: 'en-US'
    };
}

~/config/head.js


export default ({env}) => {
    // We can use env in here, to generate the config of head
}

This way we might also be able to determine the order by the destructuring parameter (like dependencies). But this might be overkill.

Also possible for modules and their options. But here is the order somewhat more important. So you will need a config/modules/index.js to specify them:


export default ({env}, load) => {
    return [
         '@nuxtjs/sentry',
         [
             '@nuxtjs/axios',
             load('~/config/modules/axios')
         ]
    ]
}

There could be a second parameter which takes care of the loading, which just calls the default export with the current configuration as parameter. Similair to plugins.

I think this is easier for new Nuxt.js users than the extend.

pi0 commented 5 years ago

Plus loading order, we can support Unix style config naming to specify priority be prefixing with numbers. Like 1-env.js / etc.

Timkor commented 5 years ago

@pi0 Actually I think it might also be possible to dynamically determine the order using a Proxy. Something like this:

function makeProxy(config) {
    return new Proxy({}, {

        get: function(obj, prop) {

            if (prop in obj) {
                return obj[prop];
            } else {

                console.log('Load: ' + prop);

                return obj[prop] = loadConfiguration(prop);
            }
        }
    });
}

var configuration = makeProxy({});

function loadConfiguration(prop) {
    return require('~config/' + prop)(configuration);
}

Of course you'd need to watch out and warn for circular deps.

Timkor commented 5 years ago

I made a Nuxt module to demonstrate a working POC of dynamic load order of the config files I suggested using a Proxy:

https://github.com/Timkor/nuxt-config

I am curious what you guys think, and if it is a possibility to add this functionality to nuxt.

nuxt-config

Nuxt Module for splitting your nuxt.config.js into multiple files.

Features

Examples:

~/config/env.js:


// Support for object
export default {
    quickBuild: true,
    sentryDSN: '...'
}

~/config/build.js:


// Support for function:
// Because of {env} this module will first import ~/config/env.js
export default ({env}) => {

    return {

        hardSource: env.quickBuild,

        extend(config, ctx) {

        }
    }
}

~/config/plugins.js:

// Support for Arrays
export default [
  '~/plugins/google-analytics.js'
]

Known issues

There is still one problem: circulair dependencies. If you would have two config functions:

~/config/foo.js:

export default (config) => {

    // Read bar
    const bar = config.bar; // This will trigger to import ~/config/bar.js
}

~/config/bar.js:

export default (config) => {

    // Read foo
    const foo = config.foo; // This will trigger to import ~/config/foo.js
}

This is not possible. So what happens now is:

What needs to be done:

The circulair dependency warning is now:

Can not early load '~/config/bar.js' because of a circulair dependency

It would be nice to have a descriptive warning which tells:

An example of a better warning can be:

Can not load '~/config/bar.js' because of a circulair dependency:
 - config.bar is referenced in ~/config/foo.js
 - config.foo is referenced in ~/config/bar.js (resolved)

Some info about how to resolve/fix this.

This can also occur with a path which is larger than 2, for instance: a -> b -> c -> a.

This can be achieved by implementing a dependency tree.

atinux commented 5 years ago

Hi @Timkor

Thank you so much for investigating in it and creating a module πŸ‘

Actually, I am trying to understand the use case of exporting a method and receiving the config as parameter. What is the real use case?

Actually, for env, I don't know if we should keep it, people can use process.env right inside their config files to conditionally update their config before exporting.

Would like some feedback from @nuxt/core-team

Timkor commented 5 years ago

@Atinux It was interresting to develop. Though not really neccesary in any way.

I thought it would be nice to somewhere have a definition of the environment variables for better IDE support and better TypeScript integration. In addition, I could think of some use cases where you would want some configuration file dependant on some other properties of the nuxt.config.js. However, those use cases could be solved one way or another.

Edit: A good use case for this functionality would be for instance:

Generating a sitemap with localisation with the following modules: https://github.com/nuxt-community/sitemap-module https://github.com/nuxt-community/nuxt-i18n

It would be nice to filter the sitemap routes based on properties of localisation (deferred from req.headers.host)

Then one would need access to the nuxt-i18n config in:

// nuxt.config.js

// Filter routes by language
{
  sitemap: {
    filter ({ routes, options }) {
      if (options.hostname === 'example.com') {
        return routes.filter(route => getNuxtI18nLocaleOf(route) === 'en')
      }
      return routes.filter(route => getNuxtI18nLocaleOf(route) === 'fr')
    }
  }
}

Just an example.

Could be written as:

// config/sitemap.js

// Filter routes by language
export default ({i18n}) => {

  function getNuxtI18nLocaleOf(route) {
      // Use i18n here to determine locale of route
      return ...
  }

  return {
    filter ({ routes, options }) {
      if (options.hostname === 'example.com') {
        return routes.filter(route => getNuxtI18nLocaleOf(route) === 'en')
      }
      return routes.filter(route => getNuxtI18nLocaleOf(route) === 'fr')
    }
  }
}
fadonascimento commented 4 years ago

Hi @Timkor

Thank you so much for investigating in it and creating a module πŸ‘

Actually, I am trying to understand the use case of exporting a method and receiving the config as parameter. What is the real use case?

Actually, for env, I don't know if we should keep it, people can use process.env right inside their config files to conditionally update their config before exporting.

Would like some feedback from @nuxt/core-team

This module seems to solve the problem with several sites in a single instance because most of the module options are configured in nuxt.config.js, but for several sites, there is a strong dependency on the host (postponed from req.headers.host ) that the site will be served, so there are few examples for multi-sites that has different settings.

AndrewBogdanovTSS commented 2 years ago

Hi guys, is this RFC still alive?

pi0 commented 2 years ago

Here is a quick recap of Nuxt 3 latest config improvements in relate to this RFC:

Ideas welcome to both nuxt/framework and unjs/c12 repos.