Skip to content

Instantly share code, notes, and snippets.

@BananaAcid
Last active October 5, 2024 20:05
Show Gist options
  • Save BananaAcid/8ff0ee91c43303a7cc7481a1989b9420 to your computer and use it in GitHub Desktop.
Save BananaAcid/8ff0ee91c43303a7cc7481a1989b9420 to your computer and use it in GitHub Desktop.
Nuxt module - simple base with auto-import

nuxt module base

A simple start with auto importing the common file types

setup

  1. follow the simple setup at nuxt.com > Modules
  2. copy the other files (at least magicImports.ts, optionally module.ts) from below to auto-import the common filetypes
  3. npm i fast-glob ... is required, as long as node:fs > glob is still experimental

The files here allow a folder structure like this, with auto importing:

image

All of these folders are optional.

Note

  1. Dynamic parameters for api and pages will work. ( [slug] and [...slugs] )
  2. apiRoot and pagesRoot can be the same! The api and pages routes will be to accompany each other, but the api routes (if being the same) will be prioritized due to the import order.
  3. Import order: Composables, Middleware, API, Plugins, Components, Pages, Styles, Scripts, Public
  4. Scripts will loaded from Public, if the file name end with .autoload.js
  5. You may always use other folders and call the needed functions to add ressources with custom options

@nuxt/kit: 3.13.1

LICENSE

MIT.

import { addPlugin, createResolver, addServerHandler, addComponent, type AddComponentOptions, extendPages, addImportsDir, addTemplate } from '@nuxt/kit';
import type { NitroEventHandler } from 'nitropack';
import { glob } from 'fast-glob';
import { basename, extname } from 'node:path';
import { stat } from 'node:fs/promises';
import type { Nuxt } from '@nuxt/schema';
const { resolve } = createResolver(import.meta.url);
export interface ModuleOptions {
apiRoot: string,
pagesRoot: string,
logName: string,
logDebug?: boolean,
_nuxt: Nuxt,
}
export default async (options: ModuleOptions) => {
const GLOB_OPTS = { onlyFiles: true };
let start = performance.now();
await handleComposables(options);
/**
* Glob proccessing
*
* fn: add handler
* alt: clearification like 'Middleware', 'API'
* pathKey: specific key used for the file path in handler option
* handler: handler function to modify options, must return inOpts: (inOpt: any, path: string) => inOpt
*/
let globSetupConfigs = [
{ fn: addTypes, pathKey: 'filePath', handler: /* pass nuxt ref */ (inOpt: AddTypesOptions, path: string) => { inOpt._nuxt = options._nuxt; return inOpt } },
{ fn: addServerHandler, alt: 'Middleware', pathKey: 'handler', handler: /* clear route: no route === middleware */ (inOpt: NitroEventHandler, path: string) => { inOpt.route = ''; return inOpt } },
{ fn: addServerHandler, alt: 'API', pathKey: 'handler', handler: /* deduct route from path */ (inOpt: NitroEventHandler, path: string) => { inOpt.route = options.apiRoot + fixRoutes(path.replace(resolve('./server/api'), '').replace(extname(path), '').replace(/\/index$/, '')); return inOpt } },
{ fn: addPlugin, pathKey: 'src', handler: /* do nothing */ (inOpt: any, path: string) => inOpt },
{ fn: addComponent, pathKey: 'filePath', handler: /* deduct component name from filename */ (inOpt: AddComponentOptions, path: string) => { inOpt.name = basename(path).replace(extname(path), ''); return inOpt } },
{ fn: addPage, pathKey: 'filePath', handler: /* pass module config: root */ (inOpt: AddPageOptions, path: string) => { inOpt.pagesRoot = options.pagesRoot; inOpt.logName = options.logName; return inOpt } },
{ fn: addStyle, alt: 'Style', pathKey: 'filePath', handler: /* pass nuxt ref */ (inOpt: AddAssetsOptions, path: string) => { inOpt._nuxt = options._nuxt; return inOpt } },
{ fn: addScript, alt: 'Script', pathKey: 'filePath', handler: /* pass nuxt ref */ (inOpt: AddAssetsOptions, path: string) => { inOpt._nuxt = options._nuxt; return inOpt } },
];
// same order as above: find all files in parallel, do not search subfolders (except for api, pages, assets)
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
let globResults = await Promise.all([
glob(resolve('./types') + '/*.d.ts', GLOB_OPTS),
glob(resolve('./server/middleware') + '/*.(ts|mjs)', GLOB_OPTS),
glob(resolve('./server/api') + '/**/*.(ts|mjs)', GLOB_OPTS),
glob(resolve('./app/plugins') + '/*.(ts|mjs)', GLOB_OPTS),
glob(resolve('./app/components') + '/*.vue', GLOB_OPTS),
glob(resolve('./app/pages') + '/**/*.vue', GLOB_OPTS),
glob(resolve('./app/styles') + '/**/*.(css|less|sass|scss|styl|stylus|pcss|postcss)', GLOB_OPTS),
glob(resolve('./app/public') + '/**/*.autoload.(js|mjs)', GLOB_OPTS),
]);
options.logDebug && console.log('Files to import', globResults);
// initiate all files, stay in order while processing
// due to aync, no .forEach() calls !
for (let idx = 0; idx < globResults.length; idx++) {
let result = globResults[idx];
let config = globSetupConfigs[idx];
for (const path of result) {
let option = config.handler({
[config.pathKey]: path,
}, path);
// addComponent returns a promise ...
let ret = await Promise.resolve(config.fn(option));
options.logDebug && console.log(config.fn.name, config.alt ? `(${config.alt})` : '', '\n', ret || option);
}
}
await handlePublic(options);
console.info(`${options.logName} finished importing in`, (performance.now() - start).toFixed(2) + 'ms');
};
export interface AddPageOptions {
filePath: string,
pagesRoot: string,
logName: string,
}
function addPage(opts: AddPageOptions) {
let { filePath, pagesRoot, logName } = opts;
// deduct route from path
let relFile = filePath.replace(resolve('./app/pages'), '');
let fileBasename = relFile.replace(extname(filePath), '');
let relPath = relFile.replace(extname(filePath), '');
let name = (logName + relPath).replace(/[^a-zA-Z0-9-_]+/g, '-').replace(/-{2,}/g, '-');
if (fileBasename === '/index') {
relPath = relPath.replace(/\/index$/, '');
}
relPath = fixRoutes(relPath);
// prepare page config
let uri = pagesRoot.replace('\\', '/') + relPath;
let item = { file: opts.filePath, name, path: uri };
// add page to router
extendPages((pages) => { pages.push(item) });
// return page config
return item;
}
export interface AddAssetsOptions {
filePath: string,
_nuxt: Nuxt,
}
function addStyle(opts: AddAssetsOptions) {
let { filePath, _nuxt } = opts;
_nuxt.options.css.push(filePath);
return { filePath };
}
function addScript(opts: AddAssetsOptions) {
let { filePath, _nuxt } = opts;
// filepath must be relative to the public folder
let uri = filePath.replace(resolve('./app/public'), '');
_nuxt.options.app.head.script ||= [];
_nuxt.options.app.head.script.push({ src: uri });
return { filePath };
}
function fixRoutes(uri: string) {
/*
fix nuxt route syntax (file system compatibile) to vue route syntax
1. file location: "api/[slug]" to needed route: "/api/:slug"
2. file location: "api/[...slug]" to needed route: "/api/**:slug"
*/
return uri.replace(/\[(\.\.\.)?([^\/\]]+)\]/g, (_, dots, slug) => {
return dots ? `**:${slug}` : `:${slug}`;
});
}
async function handleComposables(options: ModuleOptions) {
let uri = resolve('./composables');
// guard against no public assets
if (!(await stat(uri).catch(() => { }))?.isDirectory()) {
options.logDebug && console.info(options.logName, 'has no composables');
return;
}
addImportsDir(resolve('./composables'));
options.logDebug && console.info(options.logName, 'added composables');
}
async function handlePublic(options: ModuleOptions) {
let uri = resolve('./app/public');
// guard against no public assets
if (!(await stat(uri).catch(() => { }))?.isDirectory()) {
options.logDebug && console.info(options.logName, 'has no public assets');
return;
}
options._nuxt.hook('nitro:config', async (nitroConfig) => {
nitroConfig.publicAssets ||= []
nitroConfig.publicAssets.push({
dir: uri,
maxAge: 60 * 60 * 24 * 365 // 1 year
});
});
options.logDebug && console.info(options.logName, 'added public assets');
}
export interface AddTypesOptions {
filePath: string,
_nuxt: Nuxt,
}
async function addTypes(opts: AddTypesOptions) {
const template = addTemplate({
/* template options */
src: opts.filePath,
});
opts._nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: template.dst })
});
return { filePath: opts.filePath };
}
import { defineNuxtModule, createResolver } from '@nuxt/kit';
import magicImports from './runtime/magicImports';
// Module options TypeScript interface definition
export interface ModuleOptions {
apiRoot: string,
pagesRoot: string,
logDebug?: boolean,
}
// extracted here, because I could not find a way to access the meta in setup()
const MODULE_META = {
name: 'nuxt-some-module',
configKey: 'someModule',
};
export default defineNuxtModule<ModuleOptions>({
meta: MODULE_META,
// Default configuration options of the Nuxt module
defaults: {
apiRoot: '/api-test',
pagesRoot: '/test',
logDebug: false,
},
async setup(_options, _nuxt) {
const { resolve } = createResolver(import.meta.url);
let start = performance.now();
await magicImports({
apiRoot: _options.apiRoot,
pagesRoot: _options.pagesRoot,
logName: MODULE_META.name,
logDebug: _options.logDebug,
_nuxt,
});
// ...
console.info(`${MODULE_META.name} finished loading in`, (performance.now() - start).toFixed(2) + 'ms');
},
});
<template>
<div>ABC for da win!</div>
</template>
import { defineEventHandler } from 'h3';
export default defineEventHandler((event) => {
return {
hello: 'world',
};
});
// import { defineEventHandler } from 'h3';
//
// export default defineEventHandler((event) => {
// console.log(event.node.req.method, event.node.res.statusCode ?? '', event.node.req.url);
// });
import { fromNodeMiddleware } from 'h3'
export default fromNodeMiddleware((req, res, next) => {
let start = performance.now();
next();
let end = (performance.now() - start).toFixed(3) + 'ms';
console.log(req.method, res.statusCode ?? '', req.url, end);
});
import { defineNuxtPlugin } from '#app';
export default defineNuxtPlugin((_nuxtApp) => {
console.log(' - Plugin injected by my-module!');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment