|
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 }; |
|
} |