forked from nuxt/framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: components discovery (nuxt#243)
Co-authored-by: Daniel Roe <daniel@roe.dev>
- Loading branch information
Showing
17 changed files
with
370 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ dist | |
node_modules | ||
_templates | ||
schema | ||
**/*.tmpl.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<template> | ||
<div> | ||
<hello-world /> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<template> | ||
<div> | ||
This is HelloWorld component! | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { defineNuxtConfig } from '@nuxt/kit' | ||
|
||
export default defineNuxtConfig({ | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"name": "example-with-components", | ||
"private": true, | ||
"devDependencies": { | ||
"nuxt3": "latest" | ||
}, | ||
"scripts": { | ||
"dev": "nu dev", | ||
"build": "nu build", | ||
"start": "node .output/server" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { defineBuildConfig } from 'unbuild' | ||
|
||
export default defineBuildConfig({ | ||
declaration: true, | ||
entries: [ | ||
'src/module', | ||
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' } | ||
] | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./dist/module') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "@nuxt/component-discovery", | ||
"version": "0.1.0", | ||
"repository": "nuxt/framework", | ||
"license": "MIT", | ||
"types": "./dist/module.d.ts", | ||
"files": [ | ||
"dist", | ||
"module.js" | ||
], | ||
"scripts": { | ||
"prepack": "unbuild" | ||
}, | ||
"dependencies": { | ||
"@nuxt/kit": "^0.6.3", | ||
"globby": "^11.0.3", | ||
"scule": "^0.2.1", | ||
"ufo": "^0.7.5", | ||
"upath": "^2.0.1" | ||
}, | ||
"devDependencies": { | ||
"unbuild": "^0.3.1" | ||
}, | ||
"peerDependencies": { | ||
"vue": "3.1.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import fs from 'fs' | ||
import { defineNuxtModule, resolveAlias } from '@nuxt/kit' | ||
import { resolve, dirname } from 'upath' | ||
import { scanComponents } from './scan' | ||
import type { ComponentsDir } from './types' | ||
|
||
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string' | ||
const getDir = (p: string) => fs.statSync(p).isDirectory() ? p : dirname(p) | ||
|
||
export default defineNuxtModule({ | ||
name: 'components', | ||
defaults: { | ||
dirs: ['~/components'] | ||
}, | ||
setup (options, nuxt) { | ||
let componentDirs = [] | ||
|
||
// Resolve dirs | ||
nuxt.hook('app:resolve', async () => { | ||
await nuxt.callHook('components:dirs', options.dirs) | ||
|
||
componentDirs = options.dirs.filter(isPureObjectOrString).map((dir) => { | ||
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir } | ||
const dirPath = getDir(resolveAlias(dirOptions.path, nuxt.options.alias)) | ||
const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto' | ||
const extensions = dirOptions.extensions || ['vue'] // TODO: nuxt extensions and strip leading dot | ||
|
||
dirOptions.level = Number(dirOptions.level || 0) | ||
|
||
const enabled = fs.existsSync(dirPath) | ||
if (!enabled && dirOptions.path !== '~/components') { | ||
// eslint-disable-next-line no-console | ||
console.warn('Components directory not found: `' + dirPath + '`') | ||
} | ||
|
||
return { | ||
...dirOptions, | ||
enabled, | ||
path: dirPath, | ||
extensions, | ||
pattern: dirOptions.pattern || `**/*.{${extensions.join(',')},}`, | ||
ignore: [ | ||
'**/*.stories.{js,ts,jsx,tsx}', // ignore storybook files | ||
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', // ignore mixins | ||
'**/*.d.ts', // .d.ts files | ||
// TODO: support nuxt ignore patterns | ||
...(dirOptions.ignore || []) | ||
], | ||
transpile: (transpile === 'auto' ? dirPath.includes('node_modules') : transpile) | ||
} | ||
}).filter(d => d.enabled) | ||
|
||
nuxt.options.build!.transpile!.push(...componentDirs.filter(dir => dir.transpile).map(dir => dir.path)) | ||
}) | ||
|
||
// Scan components and add to plugin | ||
nuxt.hook('app:templates', async (app) => { | ||
const components = await scanComponents(componentDirs, nuxt.options.srcDir!) | ||
await nuxt.callHook('components:extend', components) | ||
if (!components.length) { | ||
return | ||
} | ||
|
||
app.templates.push({ | ||
path: 'components.mjs', | ||
src: resolve(__dirname, 'runtime/components.tmpl.mjs'), | ||
data: { components } | ||
}) | ||
app.templates.push({ | ||
path: 'components.d.ts', | ||
src: resolve(__dirname, 'runtime/components.tmpl.d.ts'), | ||
data: { components } | ||
}) | ||
app.plugins.push({ src: '#build/components' }) | ||
}) | ||
|
||
// Watch for changes | ||
nuxt.hook('builder:watch', async (event, path) => { | ||
if (!['add', 'unlink'].includes(event)) { | ||
return | ||
} | ||
const fPath = resolve(nuxt.options.rootDir, path) | ||
if (componentDirs.find(dir => fPath.startsWith(dir.path))) { | ||
await nuxt.callHook('builder:generateApp') | ||
} | ||
}) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
declare module 'vue' { | ||
export interface GlobalComponents { | ||
<%= components.map(c => { | ||
return ` ${c.pascalName}: typeof import('${c.filePath}')['${c.export}']` | ||
}).join(',\n') %> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
|
||
import { defineAsyncComponent } from 'vue' | ||
|
||
const components = { | ||
<%= components.map(c => { | ||
const exp = c.export === 'default' ? `c.default || c` : `c['${c.export}']` | ||
const magicComments = [ | ||
`webpackChunkName: "${c.chunkName}"`, | ||
c.prefetch === true || typeof c.prefetch === 'number' ? `webpackPrefetch: ${c.prefetch}` : false, | ||
c.preload === true || typeof c.preload === 'number' ? `webpackPreload: ${c.preload}` : false, | ||
].filter(Boolean).join(', ') | ||
|
||
return ` ${c.pascalName}: defineAsyncComponent(() => import('${c.filePath}' /* ${magicComments} */).then(c => ${exp}))` | ||
}).join(',\n') %> | ||
} | ||
|
||
export default function (nuxt) { | ||
for (const name in components) { | ||
nuxt.app.component(name, components[name]) | ||
nuxt.app.component('Lazy' + name, components[name]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { basename, extname, join, dirname, relative } from 'upath' | ||
import globby from 'globby' | ||
import { pascalCase, splitByCase } from 'scule' | ||
import type { ScanDir, Component } from './types' | ||
|
||
export function sortDirsByPathLength ({ path: pathA }: ScanDir, { path: pathB }: ScanDir): number { | ||
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length | ||
} | ||
|
||
// vue@2 src/shared/util.js | ||
// TODO: update to vue3? | ||
function hyphenate (str: string):string { | ||
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase() | ||
} | ||
|
||
export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<Component[]> { | ||
const components: Component[] = [] | ||
const filePaths = new Set<string>() | ||
const scannedPaths: string[] = [] | ||
|
||
for (const { path, pattern, ignore = [], prefix, extendComponent, pathPrefix, level, prefetch = false, preload = false } of dirs.sort(sortDirsByPathLength)) { | ||
const resolvedNames = new Map<string, string>() | ||
|
||
for (const _file of await globby(pattern!, { cwd: path, ignore })) { | ||
const filePath = join(path, _file) | ||
|
||
if (scannedPaths.find(d => filePath.startsWith(d))) { | ||
continue | ||
} | ||
|
||
if (filePaths.has(filePath)) { continue } | ||
filePaths.add(filePath) | ||
|
||
// Resolve componentName | ||
const prefixParts = ([] as string[]).concat( | ||
prefix ? splitByCase(prefix) : [], | ||
(pathPrefix !== false) ? splitByCase(relative(path, dirname(filePath))) : [] | ||
) | ||
let fileName = basename(filePath, extname(filePath)) | ||
if (fileName.toLowerCase() === 'index') { | ||
fileName = pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ | ||
} | ||
const fileNameParts = splitByCase(fileName) | ||
|
||
const componentNameParts: string[] = [] | ||
|
||
while (prefixParts.length && | ||
(prefixParts[0] || '').toLowerCase() !== (fileNameParts[0] || '').toLowerCase() | ||
) { | ||
componentNameParts.push(prefixParts.shift()!) | ||
} | ||
|
||
const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts) | ||
|
||
if (resolvedNames.has(componentName)) { | ||
// eslint-disable-next-line no-console | ||
console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` + | ||
`\n - ${filePath}` + | ||
`\n - ${resolvedNames.get(componentName)}` | ||
) | ||
continue | ||
} | ||
resolvedNames.set(componentName, filePath) | ||
|
||
const pascalName = pascalCase(componentName) | ||
const kebabName = hyphenate(componentName) | ||
const shortPath = relative(srcDir, filePath) | ||
const chunkName = 'components/' + kebabName | ||
|
||
let component: Component = { | ||
filePath, | ||
pascalName, | ||
kebabName, | ||
chunkName, | ||
shortPath, | ||
export: 'default', | ||
global: Boolean(global), | ||
level: Number(level), | ||
prefetch: Boolean(prefetch), | ||
preload: Boolean(preload) | ||
} | ||
|
||
if (typeof extendComponent === 'function') { | ||
component = (await extendComponent(component)) || component | ||
} | ||
|
||
// Check if component is already defined, used to overwite if level is inferiour | ||
const definedComponent = components.find(c => c.pascalName === component.pascalName) | ||
if (definedComponent && component.level < definedComponent.level) { | ||
Object.assign(definedComponent, component) | ||
} else if (!definedComponent) { | ||
components.push(component) | ||
} | ||
} | ||
|
||
scannedPaths.push(path) | ||
} | ||
|
||
return components | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
export interface Component { | ||
pascalName: string | ||
kebabName: string | ||
export: string | ||
filePath: string | ||
shortPath: string | ||
chunkName: string | ||
level: number | ||
prefetch: boolean | ||
preload: boolean | ||
|
||
/** @deprecated */ | ||
import?: string | ||
/** @deprecated */ | ||
asyncImport?: string | ||
/** @deprecated */ | ||
global?: boolean | ||
/** @deprecated */ | ||
async?: boolean | ||
} | ||
|
||
export interface ScanDir { | ||
path: string | ||
pattern?: string | string[] | ||
ignore?: string[] | ||
prefix?: string | ||
pathPrefix?: boolean | ||
level?: number | ||
prefetch?: boolean | ||
preload?: boolean | ||
extendComponent?: (component: Component) => Promise<Component | void> | (Component | void) | ||
/** @deprecated */ | ||
global?: boolean | 'dev' | ||
} | ||
|
||
export interface ComponentsDir extends ScanDir { | ||
watch?: boolean | ||
extensions?: string[] | ||
transpile?: 'auto' | boolean | ||
} | ||
|
||
type componentsDirHook = (dirs: ComponentsDir[]) => void | Promise<void> | ||
type componentsExtendHook = (components: (ComponentsDir | ScanDir)[]) => void | Promise<void> | ||
|
||
export interface Options { | ||
dirs: (string | ComponentsDir)[] | ||
loader: Boolean | ||
} | ||
|
||
declare module '@nuxt/kit' { | ||
interface NuxtOptions { | ||
components: boolean | Options | Options['dirs'] | ||
} | ||
interface NuxtOptionsHooks { | ||
'components:dirs'?: componentsDirHook | ||
'components:extend'?: componentsExtendHook | ||
components?: { | ||
dirs?: componentsDirHook | ||
extend?: componentsExtendHook | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
<template> | ||
<div> | ||
Hello | ||
<HelloWorld /> | ||
</div> | ||
</template> | ||
|
||
|
Oops, something went wrong.