├── docs ├── .vitepress │ ├── .gitignore │ └── config.ts ├── getting-started.md ├── package.json └── index.md ├── packages ├── server-block-nuxt │ ├── .gitignore │ ├── src │ │ ├── volar.ts │ │ ├── runtime │ │ │ └── plugin.ts │ │ ├── utils.ts │ │ └── module.ts │ ├── build.config.ts │ ├── test │ │ └── module.test.ts │ ├── scripts │ │ ├── readme.ts │ │ └── dts.ts │ └── package.json ├── extract-sfc-block │ ├── unplugin │ │ ├── vite.ts │ │ └── unplugin.ts │ ├── build.config.ts │ ├── scripts │ │ └── dts.ts │ ├── package.json │ ├── README.md │ └── src │ │ ├── utils.ts │ │ ├── cache.ts │ │ └── index.ts └── sfc-server-volar │ ├── README.md │ ├── build.config.ts │ ├── scripts │ └── dts.ts │ ├── package.json │ └── src │ └── index.ts ├── playgrounds ├── basic │ ├── .npmrc │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ ├── README.md │ ├── pages │ │ ├── todos.vue │ │ ├── nested │ │ │ └── hello.vue │ │ ├── message.vue │ │ └── index.vue │ ├── package.json │ └── nuxt.config.ts └── prisma │ ├── .env │ ├── .npmrc │ ├── app.vue │ ├── server │ └── tsconfig.json │ ├── prisma │ ├── dev.db │ ├── migrations │ │ ├── migration_lock.toml │ │ └── 20230713165002_init │ │ │ └── migration.sql │ ├── schema.prisma │ └── seed.ts │ ├── public │ └── favicon.ico │ ├── tsconfig.json │ ├── prisma.ts │ ├── lib │ └── prisma.ts │ ├── nuxt.config.ts │ ├── .gitignore │ ├── pages │ └── index.vue │ ├── package.json │ └── README.md ├── .eslintrc ├── CODEOWNERS ├── .npmrc ├── test ├── package.json ├── fixtures │ └── basic │ │ ├── app.vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ └── server │ │ └── api │ │ └── todos.ts └── basic.test.ts ├── renovate.json ├── taze.config.ts ├── pnpm-workspace.yaml ├── .gitignore ├── .editorconfig ├── .vscode └── settings.json ├── turbo.json ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ ├── release.yaml │ └── ci.yaml ├── package.json ├── README.md └── CHANGELOG.md /docs/.vitepress/.gitignore: -------------------------------------------------------------------------------- 1 | cache 2 | dist -------------------------------------------------------------------------------- /packages/server-block-nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /playgrounds/basic/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoisted=true -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@hebilicious"] 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hebilicious -------------------------------------------------------------------------------- /playgrounds/prisma/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./dev.db" 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-module-templates-test" 3 | } 4 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## 📦 Installation 4 | 5 | ... 6 | -------------------------------------------------------------------------------- /playgrounds/prisma/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>Hebilicious/renovate-config" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /taze.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "taze" 2 | 3 | export default defineConfig({}) 4 | -------------------------------------------------------------------------------- /playgrounds/prisma/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playgrounds/prisma/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs/** 3 | - packages/** 4 | - playgrounds/** 5 | - test/** 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/unplugin/vite.ts: -------------------------------------------------------------------------------- 1 | import unplugin from "./unplugin" 2 | 3 | export default unplugin.vite 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .nuxt 4 | .eslintcache 5 | .vitepress/cache 6 | .vitepress/dist 7 | .turbo 8 | .output 9 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hebilicious/server-block-nuxt/HEAD/playgrounds/prisma/prisma/dev.db -------------------------------------------------------------------------------- /packages/server-block-nuxt/src/volar.ts: -------------------------------------------------------------------------------- 1 | // @todo bundle the volar extension ? 2 | // export * from "@hebilicious/sfc-server-volar" 3 | -------------------------------------------------------------------------------- /playgrounds/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hebilicious/server-block-nuxt/HEAD/playgrounds/basic/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /playgrounds/prisma/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hebilicious/server-block-nuxt/HEAD/playgrounds/prisma/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/prisma/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/sfc-server-volar/README.md: -------------------------------------------------------------------------------- 1 | # Extract Server Block Volar 2 | 3 | This is a volar plugin that enables IDE features in SFC `server` blocks. -------------------------------------------------------------------------------- /test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: ["../../../packages/server-block-nuxt/src/module.ts"] 3 | }) 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-fixture", 3 | "private": true, 4 | "dependencies": { 5 | "nuxt": "3.6.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/basic/server/api/todos.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => { 2 | return [{ id: 1, todo: "Hello" }, { id: 2, todo: "World" }] 3 | }) 4 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /packages/server-block-nuxt/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild" 2 | 3 | export default defineBuildConfig({ 4 | entries: ["src/module"], 5 | failOnWarn: false 6 | }) 7 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/test/module.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | 3 | describe("all", () => { 4 | it("tests", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma.ts: -------------------------------------------------------------------------------- 1 | import PrismaClientPkg from "@prisma/client" 2 | 3 | const PrismaClient = PrismaClientPkg.PrismaClient 4 | 5 | const prisma = new PrismaClient() 6 | 7 | export { 8 | prisma 9 | } 10 | -------------------------------------------------------------------------------- /playgrounds/prisma/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import PrismaClientPkg from "@prisma/client" 2 | 3 | const PrismaClient = PrismaClientPkg.PrismaClient 4 | 5 | const prisma = new PrismaClient() 6 | 7 | export { 8 | prisma 9 | } 10 | -------------------------------------------------------------------------------- /playgrounds/prisma/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: [ 4 | "../../packages/server-block-nuxt/src/module" 5 | ], 6 | devtools: { enabled: true } 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /playgrounds/prisma/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | .cache 6 | dist 7 | 8 | # Node dependencies 9 | node_modules 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Misc 16 | .DS_Store 17 | .fleet 18 | .idea 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vitepress dev", 6 | "build": "vitepress build", 7 | "preview": "vitepress preview" 8 | }, 9 | "devDependencies": { 10 | "vitepress": "1.0.0-rc.40" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "workbench.colorCustomizations": {}, 8 | "inline-bookmarks.view.showVisibleFilesOnly": false 9 | } 10 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "dev": { 5 | "cache": false, 6 | "persistent": true 7 | }, 8 | "build": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["dist/**"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/scripts/readme.ts: -------------------------------------------------------------------------------- 1 | import { copyFileSync } from "node:fs" 2 | import { resolve } from "node:path" 3 | import { fileURLToPath } from "node:url" 4 | 5 | const dir = fileURLToPath(new URL("..", import.meta.url)) 6 | copyFileSync(resolve(dir, "../../README.md"), resolve(dir, "./README.md")) 7 | -------------------------------------------------------------------------------- /playgrounds/basic/README.md: -------------------------------------------------------------------------------- 1 | # AuthJS Nuxt 2 | 3 | This is a simple example to demonstrate how to use the module. 4 | ## Dependencies Caveats 5 | 6 | The dev dependencies are required for the build to work with pnpm. 7 | If you are using npm or `--shamefully-hoist=true`, you should be able to remove them from the `package.json` file. -------------------------------------------------------------------------------- /playgrounds/basic/pages/todos.vue: -------------------------------------------------------------------------------- 1 | 2 | export const GET = defineEventHandler((event) => { 3 | return [{ id: 1, content: "Hello World" }] 4 | }) 5 | 6 | 7 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma/migrations/20230713165002_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Todo" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" DATETIME NOT NULL, 6 | "title" TEXT NOT NULL, 7 | "completed" BOOLEAN NOT NULL, 8 | "content" TEXT 9 | ); 10 | -------------------------------------------------------------------------------- /packages/sfc-server-volar/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild" 2 | 3 | export default defineBuildConfig({ 4 | entries: ["src/index"], 5 | externals: [ 6 | "@vue/language-core" 7 | ], 8 | clean: true, 9 | declaration: true, 10 | rollup: { 11 | emitCJS: true, 12 | inlineDependencies: true 13 | }, 14 | failOnWarn: false 15 | }) 16 | -------------------------------------------------------------------------------- /playgrounds/basic/pages/nested/hello.vue: -------------------------------------------------------------------------------- 1 | 2 | export const GET = defineEventHandler((event) => { 3 | return "We're here now." 4 | }) 5 | 6 | 7 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /playgrounds/prisma/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | import { prisma } from '~/lib/prisma' 3 | 4 | export const GET = defineEventHandler(() => prisma.todo.findMany()) 5 | 6 | 7 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/src/runtime/plugin.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from "#imports" 2 | 3 | export default defineNuxtPlugin(async (nuxt) => { 4 | // @todo this may need to be fixed in Nuxt 5 | nuxt.hook("app:suspense:resolve", async () => { 6 | await nuxt.callHook("app:data:refresh") 7 | // eslint-disable-next-line no-console 8 | console.log("[server-block-nuxt] data successfully loaded") 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /playgrounds/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-playground", 3 | "private": true, 4 | "scripts": { 5 | "build": "NITRO_PRESET=cloudflare_pages nuxt build", 6 | "dev": "nuxt dev", 7 | "generate": "nuxt generate", 8 | "preview": "nuxt preview" 9 | }, 10 | "devDependencies": { 11 | "@hebilicious/sfc-server-volar": "latest", 12 | "@nuxt/devtools": "^2.3.0", 13 | "nuxt": "latest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playgrounds/basic/pages/message.vue: -------------------------------------------------------------------------------- 1 | 2 | const message = "Hello World!!!" 3 | const bye = "bye!" 4 | export const GET = defineEventHandler(() =>({ message })) 5 | export const POST = defineEventHandler(() => ({ message: bye })) 6 | export const loader = defineEventHandler(() => ({ loaderData: "TestData" })) 7 | 8 | 9 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./packages/server-block-nuxt/.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "#imports": ["./packages/server-block-nuxt/node_modules/nuxt/app.d.ts"], 7 | "#app": ["./packages/server-block-nuxt/node_modules/nuxt/dist/app"] 8 | } 9 | }, 10 | "exclude": [ 11 | "**/node_modules", 12 | "**/dist", 13 | "**/playground", 14 | "**/playgrounds", 15 | "**/docs", 16 | "**/test/fixtures/**" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /playgrounds/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: [ 4 | "../../packages/server-block-nuxt/src/module" 5 | // "@example/server-block-nuxt" 6 | ], 7 | app: { 8 | head: { 9 | link: [{ rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css" }] 10 | } 11 | }, 12 | devtools: { 13 | enabled: true 14 | }, 15 | experimental: { 16 | renderJsonPayloads: true 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild" 2 | 3 | export default defineBuildConfig({ 4 | entries: ["src/index"], 5 | externals: [ 6 | "vite", 7 | "@vue/compiler-sfc" 8 | // "vue/compiler-sfc" 9 | // "@vue/language-core" 10 | // "@vue/shared" 11 | // "@vitejs/plugin-vue" 12 | // "consola" 13 | ], 14 | clean: true, 15 | declaration: true, 16 | rollup: { 17 | emitCJS: true, 18 | inlineDependencies: true 19 | }, 20 | failOnWarn: false 21 | }) 22 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Todo { 14 | id Int @id @default(autoincrement()) 15 | createdAt DateTime @default(now()) 16 | updatedAt DateTime @updatedAt 17 | title String 18 | completed Boolean 19 | content String? 20 | } 21 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url" 2 | import { describe, expect, it } from "vitest" 3 | import { $fetch, setup } from "@nuxt/test-utils" 4 | 5 | describe("basic test", async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL("./fixtures/basic", import.meta.url)) 8 | }) 9 | 10 | it("displays data", async () => { 11 | // Get response to a server-rendered page with `$fetch`. 12 | const html = await $fetch("/") 13 | expect(html).toContain("Hello") 14 | expect(html).toContain("World") 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /playgrounds/basic/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | const message = "Hello World!!!" 3 | export const GET = defineEventHandler(() => ({ message })) 4 | 5 | 6 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /playgrounds/prisma/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@prisma/client" 2 | import { PrismaClient } from "@prisma/client" 3 | 4 | const prisma = new PrismaClient() 5 | 6 | const todoData: Prisma.TodoCreateInput[] = [{ title: "Milk", content: "Buy milk", completed: false }] 7 | 8 | async function main() { 9 | // console.log("Start seeding ...") 10 | for (const t of todoData) { 11 | await prisma.todo.create({ 12 | data: t 13 | }) 14 | // console.log(`Created todo with id: ${todo.id}`) 15 | } 16 | // console.log("Seeding finished.") 17 | } 18 | 19 | main() 20 | .catch((e) => { 21 | console.error(e) 22 | process.exit(1) 23 | }) 24 | .finally(async () => { 25 | await prisma.$disconnect() 26 | }) 27 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Nuxt Module Template" 7 | text: "A template for your Nuxt modules" 8 | tagline: Use Nuxt module template to start your Nuxt projects. 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /getting-started 13 | 14 | features: 15 | - title: Comprehensive Documentation 📚 16 | details: Access detailed documentation tailored to your needs, right when you need it. 17 | - title: Customizable 🔧 18 | details: Adapt Nuxt Module Template to fit your specific requirements with ease. 19 | - title: Extensible & Hackable 🔌 20 | details: Expand Nuxt Module Template's capabilities with custom integrations and modules. 21 | 22 | --- -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress" 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "Nuxt Module Template", 6 | description: "A template for creating Nuxt modules", 7 | themeConfig: { 8 | // https://vitepress.dev/reference/default-theme-config 9 | nav: [ 10 | { text: "Home", link: "/" }, 11 | { text: "Get Started", link: "/getting-started" } 12 | ], 13 | 14 | sidebar: [ 15 | { 16 | // text: 'AuthJS', 17 | items: [ 18 | { text: "Get Started", link: "/getting-started" } 19 | ] 20 | } 21 | ], 22 | 23 | socialLinks: [ 24 | { icon: "github", link: "https://github.com/Hebilicious/nuxt-module-template" } 25 | ] 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /playgrounds/prisma/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-playground", 3 | "private": true, 4 | "scripts": { 5 | "build": "nuxt build", 6 | "dev": "nuxt dev", 7 | "generate": "nuxt generate", 8 | "preview": "nuxt preview", 9 | "prisma": "nuxt prepare && npx prisma migrate dev --name init && npx prisma db seed" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^6.5.0" 13 | }, 14 | "devDependencies": { 15 | "@hebilicious/server-block-nuxt": "latest", 16 | "@hebilicious/sfc-server-volar": "latest", 17 | "@nuxt/devtools": "latest", 18 | "@types/node": "^22.13.9", 19 | "esno": "^4.8.0", 20 | "nuxt": "^3.9.3", 21 | "prisma": "^6.5.0" 22 | }, 23 | "prisma": { 24 | "seed": "esno prisma/seed.ts", 25 | "schema": "./prisma/schema.prisma" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/sfc-server-volar/scripts/dts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { copyFileSync } from "node:fs" 3 | import { resolve } from "node:path" 4 | import { fileURLToPath } from "node:url" 5 | 6 | import fg from "fast-glob" 7 | 8 | const dir = fileURLToPath(new URL("..", import.meta.url)) 9 | 10 | const dts = await fg(resolve(dir, "dist/**/*.d.ts")) 11 | 12 | // Create MTS and CTS files, see https://github.com/unjs/unbuild/issues/238 & https://github.com/gvergnaud/ts-pattern/pull/160 13 | const dmts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.mts"))) // should use .mjs extensions ? 14 | const dcts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.cts"))) // should use .cjs extensions ? 15 | 16 | console.log(`Copied ${dmts.length} files from \`*.d.ts\` to \`*.d.mts\``) 17 | console.log(`Copied ${dcts.length} files from \`*.d.ts\` to \`*.d.cts\``) 18 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/scripts/dts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { copyFileSync } from "node:fs" 3 | import { resolve } from "node:path" 4 | import { fileURLToPath } from "node:url" 5 | 6 | import fg from "fast-glob" 7 | 8 | const dir = fileURLToPath(new URL("..", import.meta.url)) 9 | 10 | const dts = await fg(resolve(dir, "dist/**/*.d.ts")) 11 | 12 | // Create MTS and CTS files, see https://github.com/unjs/unbuild/issues/238 & https://github.com/gvergnaud/ts-pattern/pull/160 13 | const dmts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.mts"))) // should use .mjs extensions ? 14 | const dcts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.cts"))) // should use .cjs extensions ? 15 | 16 | console.log(`Copied ${dmts.length} files from \`*.d.ts\` to \`*.d.mts\``) 17 | console.log(`Copied ${dcts.length} files from \`*.d.ts\` to \`*.d.cts\``) 18 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/scripts/dts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { copyFileSync } from "node:fs" 4 | import { resolve } from "node:path" 5 | import { fileURLToPath } from "node:url" 6 | 7 | import fg from "fast-glob" 8 | 9 | const dir = fileURLToPath(new URL("..", import.meta.url)) 10 | 11 | const dts = await fg(resolve(dir, "dist/**/*.d.ts")) 12 | 13 | // Create MTS and CTS files, see https://github.com/unjs/unbuild/issues/238 & https://github.com/gvergnaud/ts-pattern/pull/160 14 | const dmts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.mts"))) // should use .mjs extensions ? 15 | const dcts = dts.map(f => copyFileSync(f, f.replace(/\.d\.ts$/, ".d.cts"))) // should use .cjs extensions ? 16 | 17 | console.log(`Copied ${dmts.length} files from \`*.d.ts\` to \`*.d.mts\``) 18 | console.log(`Copied ${dcts.length} files from \`*.d.ts\` to \`*.d.cts\``) 19 | -------------------------------------------------------------------------------- /playgrounds/prisma/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on `http://localhost:3000`: 23 | 24 | ```bash 25 | # npm 26 | npm run dev 27 | 28 | # pnpm 29 | pnpm run dev 30 | 31 | # yarn 32 | yarn dev 33 | ``` 34 | 35 | ## Production 36 | 37 | Build the application for production: 38 | 39 | ```bash 40 | # npm 41 | npm run build 42 | 43 | # pnpm 44 | pnpm run build 45 | 46 | # yarn 47 | yarn build 48 | ``` 49 | 50 | Locally preview production build: 51 | 52 | ```bash 53 | # npm 54 | npm run preview 55 | 56 | # pnpm 57 | pnpm run preview 58 | 59 | # yarn 60 | yarn preview 61 | ``` 62 | 63 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 64 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/unplugin/unplugin.ts: -------------------------------------------------------------------------------- 1 | // import { type ResolvedConfig } from "vite" 2 | // import { createUnplugin } from "unplugin" 3 | // import createCache from "../src/cache" 4 | // import type { PluginConfig } from "../src/utils" 5 | // import { pluginName } from "../src/utils" 6 | 7 | // /** 8 | // * WIP 9 | // */ 10 | // const unplugin = createUnplugin((pluginConfig: PluginConfig) => { 11 | // let config: ResolvedConfig 12 | // let cache: ReturnType 13 | 14 | // return { 15 | // name: `unplugin-${pluginName}`, 16 | // vite: { 17 | // configResolved(resolvedConfig) { 18 | // // logger.info("Resolved config !") 19 | // config = resolvedConfig 20 | // }, 21 | // buildStart() { 22 | // // logger.info("Starting build") 23 | // cache = createCache(config) 24 | // } 25 | // } 26 | 27 | // // load(id) { 28 | 29 | // // } 30 | 31 | // // transform(code, id) { 32 | 33 | // // } 34 | 35 | // // async handleHotUpdate({ modules, read, file, server }) { 36 | // // } 37 | // } 38 | // }) 39 | 40 | // export default unplugin 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-PRESENT Hebilicious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | 23 | - name: Set node 24 | uses: actions/setup-node@v6 25 | with: 26 | node-version: 22.19.0 27 | cache: pnpm 28 | registry-url: "https://registry.npmjs.org" 29 | 30 | - run: npx changelogithub 31 | continue-on-error: false 32 | env: 33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | - name: Install Dependencies 36 | run: pnpm i 37 | 38 | - name: PNPM build 39 | run: pnpm run build 40 | 41 | - name: Publish to NPM 42 | run: pnpm -r publish --access public --no-git-checks 43 | env: 44 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 45 | NPM_CONFIG_PROVENANCE: true 46 | -------------------------------------------------------------------------------- /packages/sfc-server-volar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hebilicious/sfc-server-volar", 3 | "type": "module", 4 | "version": "0.3.4", 5 | "author": { 6 | "name": "Hebilicious", 7 | "email": "xsh4k3@gmail.com", 8 | "url": "https://twitter.com/its_hebilicious" 9 | }, 10 | "license": "MIT", 11 | "repository": "Hebilicious/server-block-nuxt", 12 | "exports": { 13 | ".": { 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "default": "./dist/index.cjs" 17 | }, 18 | "import": { 19 | "types": "./dist/index.d.mts", 20 | "default": "./dist/index.mjs" 21 | }, 22 | "types": "./dist/index.d.ts", 23 | "default": "./dist/index.mjs" 24 | } 25 | }, 26 | "main": "./dist/index.cjs", 27 | "module": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "files": [ 30 | "dist", 31 | "*.d.ts", 32 | "*.cjs", 33 | "*.mjs" 34 | ], 35 | "scripts": { 36 | "stub": "unbuild --stub", 37 | "dts": "esno scripts/dts.ts", 38 | "build": "unbuild && pnpm run dts" 39 | }, 40 | "devDependencies": { 41 | "@vue/language-core": "^1.8.27" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/sfc-server-volar/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { VueLanguagePlugin } from "@vue/language-core" 2 | 3 | const plugin: VueLanguagePlugin = () => { 4 | return { 5 | name: "sfc-server-volar", 6 | version: 1, 7 | resolveEmbeddedFile(fileName, sfc, embeddedFile) { 8 | if (embeddedFile.fileName.replace(fileName, "").match(/^\.(js|ts|jsx|tsx)$/)) { 9 | for (const block of sfc.customBlocks) { 10 | const content = embeddedFile.content 11 | if (block.type === "server") { 12 | content.push([ 13 | block.content, // text to add 14 | block.name, // source 15 | 0, // content offset in source 16 | { 17 | // language capabilities to enable in this segment 18 | hover: true, 19 | references: true, 20 | definition: true, 21 | diagnostic: true, 22 | rename: true, 23 | completion: true, 24 | semanticTokens: true 25 | } 26 | ]) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | export default plugin 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v6 23 | with: 24 | node-version: 22.19.0 25 | cache: pnpm 26 | 27 | - name: Install 28 | run: pnpm i 29 | 30 | - name: Lint 31 | run: pnpm lint 32 | 33 | test: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 38 | 39 | - name: Install pnpm 40 | uses: pnpm/action-setup@v2 41 | 42 | - name: Set node 43 | uses: actions/setup-node@v6 44 | with: 45 | node-version: 22.19.0 46 | cache: pnpm 47 | 48 | - name: Install 49 | run: pnpm i 50 | 51 | - name: Build 52 | run: pnpm build 53 | 54 | - name: Test 55 | run: pnpm test 56 | 57 | - name: Typecheck 58 | run: pnpm typecheck 59 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hebilicious/extract-sfc-block", 3 | "type": "module", 4 | "version": "0.3.4", 5 | "author": { 6 | "name": "Hebilicious", 7 | "email": "xsh4k3@gmail.com", 8 | "url": "https://twitter.com/its_hebilicious" 9 | }, 10 | "license": "MIT", 11 | "repository": "Hebilicious/server-block-nuxt", 12 | "exports": { 13 | ".": { 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "default": "./dist/index.cjs" 17 | }, 18 | "import": { 19 | "types": "./dist/index.d.mts", 20 | "default": "./dist/index.mjs" 21 | }, 22 | "types": "./dist/index.d.ts", 23 | "default": "./dist/index.mjs" 24 | } 25 | }, 26 | "main": "./dist/index.cjs", 27 | "module": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "files": [ 30 | "dist", 31 | "*.d.ts", 32 | "*.cjs", 33 | "*.mjs" 34 | ], 35 | "scripts": { 36 | "stub": "unbuild --stub", 37 | "dts": "esno scripts/dts.ts", 38 | "build": "unbuild && pnpm run dts" 39 | }, 40 | "dependencies": { 41 | "@vitejs/plugin-vue": "^5.2.1", 42 | "@vue/shared": "^3.4.15", 43 | "consola": "^3.2.3" 44 | }, 45 | "devDependencies": { 46 | "unplugin": "^2.2.0", 47 | "vite": "^6.2.2", 48 | "vue": "^3.4.15" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-module-template", 3 | "type": "module", 4 | "version": "0.3.4", 5 | "private": true, 6 | "packageManager": "pnpm@8.15.1", 7 | "scripts": { 8 | "build": "rimraf packages/*/dist && turbo run build --filter=@hebilicious*", 9 | "build:all": "turbo run build", 10 | "lint": "eslint --cache .", 11 | "lint:fix": "nr lint --fix", 12 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 13 | "release": "pnpm lint:fix && bumpp -r -x \"pnpm run changelog\" --all", 14 | "typecheck": "tsc --noEmit", 15 | "test": "vitest" 16 | }, 17 | "devDependencies": { 18 | "@antfu/ni": "^27.0.0", 19 | "@hebilicious/eslint-config": "0.0.3-beta.3", 20 | "@nuxt/test-utils": "^3.11.0", 21 | "@types/node": "^22.13.9", 22 | "bumpp": "^10.0.3", 23 | "conventional-changelog-cli": "^5.0.0", 24 | "eslint": "8.56.0", 25 | "esno": "^4.8.0", 26 | "fast-glob": "^3.3.2", 27 | "lint-staged": "^16.0.0", 28 | "pnpm": "8.15.1", 29 | "prettier": "^3.2.4", 30 | "rimraf": "^6.0.1", 31 | "simple-git-hooks": "^2.9.0", 32 | "taze": "^19.0.2", 33 | "turbo": "^1.11.3", 34 | "typescript": "^5.3.3", 35 | "unbuild": "^3.5.0", 36 | "vitest": "^3.0.8" 37 | }, 38 | "simple-git-hooks": { 39 | "pre-commit": "npx lint-staged" 40 | }, 41 | "lint-staged": { 42 | "*.{js,ts,tsx,vue,md}": [ 43 | "eslint --cache --fix" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/README.md: -------------------------------------------------------------------------------- 1 | # Extract SFC Block 2 | 3 | Extract custom blocks from `.vue` files. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i -D @hebilicious/extract-sfc-block 9 | ``` 10 | 11 | ## Usage 12 | 13 | Add to your vite configuration : 14 | 15 | ```ts 16 | import ExtractSFCBlock from "@hebilicious/extract-sfc-block" 17 | 18 | const VitePlugin = ExtractSFCBlock({ 19 | output: "server/.generated", // This is relative to the vite config directory. 20 | sourceDir: "pages", // This is relative to the vite config directory. 21 | blockType: "server", // This will match blocks. 22 | defaultPath: "api" // This will only be used if no path attribute is provided. 23 | }) 24 | ``` 25 | 26 | With a SFC `[sourceDir]/hello.vue` : 27 | 28 | ```html 29 | 30 | const message = "Hello World!!!" 31 | 32 | 35 | ``` 36 | 37 | This plugin will create a `[output]/[defaultPath]/hello.ts` file: 38 | 39 | ```ts 40 | const message = "Hello World!!!" 41 | ``` 42 | 43 | The file extension will use the lang attribute, or default to `.ts`. 44 | 45 | ### Custom Path 46 | 47 | You can customize the output path at the block level: 48 | 49 | ```html 50 | 51 | const message = "Hello World!!!" 52 | 53 | 56 | ``` 57 | 58 | This plugin will create a `somewhere/else/another-message.ts` file: 59 | 60 | ```ts 61 | const message = "Hello World!!!" 62 | ``` 63 | 64 | ## TODO 65 | 66 | - [ ] Support multiple server blocks in a single file 67 | - [ ] Refactor to unplugin 68 | - [ ] Write tests 69 | - [ ] Write docs 70 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hebilicious/server-block-nuxt", 3 | "type": "module", 4 | "version": "0.3.4", 5 | "author": { 6 | "name": "Hebilicious", 7 | "email": "xsh4k3@gmail.com", 8 | "url": "https://twitter.com/its_hebilicious" 9 | }, 10 | "license": "MIT", 11 | "repository": "Hebilicious/server-block-nuxt", 12 | "exports": { 13 | ".": { 14 | "require": { 15 | "types": "./dist/module.d.cts", 16 | "default": "./dist/module.cjs" 17 | }, 18 | "import": { 19 | "types": "./dist/module.d.mts", 20 | "default": "./dist/module.mjs" 21 | }, 22 | "types": "./dist/module.d.ts", 23 | "default": "./dist/module.mjs" 24 | } 25 | }, 26 | "main": "./dist/module.cjs", 27 | "module": "./dist/module.mjs", 28 | "types": "./dist/types.d.ts", 29 | "files": [ 30 | "dist", 31 | "*.d.ts", 32 | "*.cjs", 33 | "*.mjs" 34 | ], 35 | "scripts": { 36 | "stub": "unbuild --stub", 37 | "dts": "esno scripts/dts.ts", 38 | "readme": "esno scripts/readme.ts", 39 | "postbuild": "pnpm dts && pnpm readme", 40 | "build:module": "nuxt-build-module", 41 | "build": "rimraf dist && pnpm build:module && pnpm postbuild", 42 | "postinstall": "nuxi prepare", 43 | "dev": "nuxi dev" 44 | }, 45 | "peerDependencies": { 46 | "@hebilicious/sfc-server-volar": "latest", 47 | "h3": "*", 48 | "nitropack": "*", 49 | "nuxt": "*", 50 | "vite": "*" 51 | }, 52 | "dependencies": { 53 | "@hebilicious/extract-sfc-block": "latest", 54 | "@nuxt/kit": "3.9.3", 55 | "consola": "^3.2.3", 56 | "defu": "^6.1.4", 57 | "esbuild": "^0.25.0", 58 | "magicast": "^0.3.3" 59 | }, 60 | "devDependencies": { 61 | "@nuxt/module-builder": "^0.5.5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import type { VueQuery } from "@vitejs/plugin-vue" 3 | import type { SFCBlock } from "vue/compiler-sfc" 4 | 5 | import { createConsola } from "consola" 6 | import { 7 | parseVueRequest as _parseVueRequest 8 | } from "@vitejs/plugin-vue" 9 | import { camelize, capitalize } from "@vue/shared" 10 | 11 | export interface PluginConfig { 12 | output: string 13 | sourceDir: string 14 | blockType: string 15 | defaultPath?: string 16 | } 17 | export const pluginName = "extract-sfc-block" as const 18 | export const GENERATED_TEXT = `/** This file is auto-generated by the [${pluginName}] plugin. /!\\ Do not modify it manually ! */ \n` as const 19 | 20 | export const logger = createConsola({ 21 | defaults: { 22 | message: `[${pluginName}]` 23 | }, 24 | level: process.env.NODE_ENV === "production" ? 4 : 3 25 | }) 26 | 27 | export function pascalCase(str: string) { 28 | return capitalize(camelize(str)) 29 | } 30 | 31 | export function parseVueRequest(id: string) { 32 | return _parseVueRequest(id) as { 33 | filename: string 34 | query: Omit & { 35 | name?: string 36 | type: VueQuery["type"] | string // This can be the PluginConfig.blockType 37 | } 38 | } 39 | } 40 | 41 | // For a [name].vue file, returns name 42 | function extractVueFileName(str: string, dir: string) { 43 | return str.startsWith(dir) ? str.split(dir)[1].slice(0, -4) : null 44 | } 45 | 46 | function removeLeadingAndTrailingSlash(str: string) { 47 | const start = str.startsWith("/") ? 1 : 0 48 | const end = str.endsWith("/") ? -1 : undefined 49 | return str.slice(start, end) 50 | } 51 | export function getExtractionInfo(root: string, file: string, pluginOptions: PluginConfig, sfc: SFCBlock) { 52 | const { output, sourceDir, defaultPath } = pluginOptions 53 | const directoryPath = path.resolve(root, output) 54 | const sourceDirectory = `${root}/${sourceDir}/` 55 | const nameAndPath = typeof sfc.attrs.path === "string" ? removeLeadingAndTrailingSlash(sfc.attrs.path) : `${defaultPath ? `${defaultPath}/` : ""}${extractVueFileName(file, sourceDirectory)}` 56 | const outputPath = nameAndPath ? `${directoryPath}/${nameAndPath}.${sfc.lang ?? "ts"}` : null 57 | // console.log({ nameAndPath, sourceDirectory, file, directoryPath, outputPath }) 58 | return { 59 | outputDirectory: outputPath ? path.dirname(outputPath) : null, 60 | outputPath 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/src/cache.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-duplicates */ 2 | import fs from "node:fs" 3 | import { createRequire } from "node:module" 4 | 5 | import type { ResolvedConfig } from "vite" 6 | import type * as _compiler from "vue/compiler-sfc" 7 | import type { SFCDescriptor } from "vue/compiler-sfc" 8 | 9 | export default function createCache(config: ResolvedConfig) { 10 | const sfcCache = new Map() 11 | const compiler = resolveCompiler(config.root) 12 | 13 | return { 14 | getSFC(filename: string) { 15 | if (!sfcCache.has(filename)) { 16 | const { descriptor, errors } = compiler.parse( 17 | fs.readFileSync(filename, "utf8").toString(), 18 | { 19 | filename, 20 | sourceMap: 21 | config.command === "build" ? !!config.build.sourcemap : true, 22 | sourceRoot: config.root 23 | } 24 | ) 25 | if (errors.length > 0) throw errors[0] 26 | sfcCache.set(filename, descriptor) 27 | } 28 | return sfcCache.get(filename)! 29 | }, 30 | updateCodeSFC(filename: string, code: string) { 31 | const { descriptor, errors } = compiler.parse(code, { filename }) 32 | if (errors.length > 0) throw errors[0] 33 | sfcCache.set(filename, descriptor) 34 | return descriptor 35 | }, 36 | hasFile(filename: string) { 37 | return sfcCache.has(filename) 38 | } 39 | } 40 | } 41 | 42 | export type CompilerSfc = typeof _compiler 43 | 44 | export function resolveCompiler(root: string): CompilerSfc { 45 | // resolve from project root first, then fallback to peer dep (if any) 46 | const compiler = tryResolveCompiler(root) || tryResolveCompiler() 47 | if (!compiler) { 48 | throw new Error( 49 | "Failed to resolve vue/compiler-sfc.\n" 50 | + "@vitejs/plugin-vue requires vue (>=3.2.25) " 51 | + "to be present in the dependency tree." 52 | ) 53 | } 54 | 55 | return compiler 56 | } 57 | 58 | function tryResolveCompiler(root?: string) { 59 | const vueMeta = tryRequire("vue/package.json", root) 60 | // make sure to check the version is 3+ since 2.7 now also has vue/compiler-sfc 61 | if (vueMeta && vueMeta.version.split(".")[0] >= 3) 62 | return tryRequire("vue/compiler-sfc", root) 63 | } 64 | 65 | const _require = createRequire(import.meta.url) 66 | function tryRequire(id: string, from?: string) { 67 | try { 68 | return from 69 | ? _require(_require.resolve(id, { paths: [from] })) 70 | : _require(id) 71 | } 72 | catch { 73 | // ignore 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/extract-sfc-block/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import { existsSync, mkdirSync, writeFileSync } from "node:fs" 3 | 4 | import type { PluginOption, ResolvedConfig } from "vite" 5 | import type { SFCBlock } from "vue/compiler-sfc" 6 | 7 | import createCache from "./cache" 8 | import type { PluginConfig } from "./utils" 9 | import { GENERATED_TEXT, getExtractionInfo, logger, parseVueRequest, pluginName } from "./utils" 10 | 11 | export default function vueExtractSFCServer(pluginConfig: PluginConfig): PluginOption { 12 | const findBlockType = (block: SFCBlock) => block.type === pluginConfig.blockType 13 | let config: ResolvedConfig 14 | let cache: ReturnType 15 | return { 16 | name: `vite:${pluginName}`, 17 | 18 | configResolved(resolvedConfig) { 19 | // logger.info("Resolved config !") 20 | config = resolvedConfig 21 | }, 22 | 23 | buildStart() { 24 | // logger.info("Starting build") 25 | cache = createCache(config) 26 | }, 27 | load(id) { 28 | // Only match vue files 29 | const match = id.match(/^(.*)\/([^/]+)\.vue$/) 30 | if (!match) return 31 | let filename = id 32 | if (!filename.startsWith(config.root)) 33 | filename = path.resolve(config.root, filename) 34 | 35 | const sfc = cache.getSFC(filename) 36 | const extractBlock = sfc.customBlocks.find(findBlockType) 37 | if (extractBlock) { 38 | logger.info(`Extracting block @ ${filename}`) 39 | // logger.info(extractBlock.content) 40 | const { outputDirectory, outputPath } = getExtractionInfo(config.root, filename, pluginConfig, extractBlock) 41 | if (outputDirectory && !existsSync(outputDirectory)) mkdirSync(outputDirectory, { recursive: true }) 42 | if (outputPath) { 43 | writeFileSync(outputPath, `${GENERATED_TEXT}${extractBlock.content}`) 44 | logger.success("Generated file: ", outputPath) 45 | } 46 | } 47 | }, 48 | 49 | transform(code, id) { 50 | const request = parseVueRequest(id) 51 | if (request.query.type === pluginConfig.blockType) { 52 | logger.info(`Transforming "${pluginConfig.blockType}" block...`) 53 | return { 54 | code: "export default {}", 55 | map: { mappings: "" } 56 | } 57 | } 58 | }, 59 | 60 | async handleHotUpdate({ modules, read, file, server }) { 61 | if (!cache.hasFile(file)) return modules 62 | 63 | logger.info("Cache hit !", file) 64 | const serverModules = new Set( 65 | modules.filter(m => m.url.includes(`&type=${pluginConfig.blockType}&`)) 66 | ) 67 | const sfc = cache.getSFC(file) 68 | const extractBlock = sfc.customBlocks.find(findBlockType) 69 | if (extractBlock) { 70 | const { outputPath } = getExtractionInfo(config.root, file, pluginConfig, extractBlock) 71 | if (outputPath) { 72 | const newSFC = cache.updateCodeSFC(file, await read()) 73 | writeFileSync(outputPath, `${GENERATED_TEXT}${newSFC.customBlocks.find(findBlockType)?.content}`) 74 | logger.success("Updated file", outputPath) 75 | const mainModule = server.moduleGraph.getModuleById(file) 76 | if (mainModule) { 77 | serverModules.add(mainModule) 78 | logger.success("Updated module", mainModule.url) 79 | } 80 | } 81 | return [...serverModules] 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/server-block-nuxt/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from "node:fs" 2 | import { dirname } from "node:path" 3 | import { createConsola } from "consola" 4 | import { type ProxifiedModule, generateCode } from "magicast" 5 | import { transform } from "esbuild" 6 | 7 | const GENERATED_TEXT = "/** This file is auto-generated by the [server-block-nuxt] module. /!\\ Do not modify it manually ! */ \n" as const 8 | 9 | export const SupportedMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE"] as const 10 | 11 | export const logger = createConsola({ 12 | defaults: { 13 | message: "[server-block-nuxt]" 14 | }, 15 | level: process.env.NODE_ENV === "production" ? 4 : 3 16 | }) 17 | 18 | export const makePathShortener = (source: string) => (path: string) => path.replace(source, "") 19 | 20 | /** 21 | * Loaders 22 | * @todo this should be re-used accross modules or configurable 23 | */ 24 | 25 | const NITRO_LOADER_PREFIX = "_nitro/loader" as const 26 | 27 | function getLoaderRoute(path: string) { 28 | const route = getRoute(path) 29 | // We don't want loaders to be prefixed with /api. 30 | const loaderRoute = route.startsWith("/api") ? route.slice(4) : route 31 | return `/${NITRO_LOADER_PREFIX}${loaderRoute}` 32 | } 33 | 34 | function getLoaderDestination(filePath: string, baseDirectory: string) { 35 | const route = getRoute(filePath) 36 | // We don't want loaders to be prefixed with /api. 37 | const loaderPath = route.startsWith("/api") ? route.slice(4) : route 38 | return `${baseDirectory}/.loader${loaderPath}.get.ts` 39 | } 40 | 41 | export function getRoute(path: string) { 42 | // logger.info("Finding route ...") 43 | const routeRegex = /server\/\.generated(.*?)\.ts/ 44 | const matches = routeRegex.exec(path) 45 | // logger.info("getRoute", path) 46 | const route = matches?.[1] 47 | if (!route) throw new Error(`Could not parse action route from ${path}`) 48 | return route 49 | } 50 | 51 | function insertBeforeExtension(filePath: string, insertion: string) { 52 | // logger.info("Inserting", filePath, insertion) 53 | const lastDotIndex = filePath.lastIndexOf(".") 54 | return `${filePath.slice(0, lastDotIndex)}.${insertion}${filePath.slice(lastDotIndex)}` 55 | } 56 | 57 | async function writeFile(file: ProxifiedModule, destination: string) { 58 | const { code } = generateCode(file) 59 | const shaked = await transform(code, { treeShaking: true, loader: "ts" }) // ...we clean it with esbuild ... 60 | if (!existsSync(dirname(destination))) await fsp.mkdir(dirname(destination), { recursive: true }) 61 | await fsp.writeFile(destination, GENERATED_TEXT) 62 | await fsp.appendFile(destination, shaked.code) 63 | } 64 | 65 | export async function writeHandlers(file: ProxifiedModule, path: string, baseDirectory: string) { 66 | // logger.info("Writing handlers ...", path) 67 | const writeMethodHandler = async (method: typeof SupportedMethods[number]) => { 68 | const destination = insertBeforeExtension(path, method.toLowerCase()) 69 | file.exports.default = file.exports[method] 70 | delete file.exports[method] // @todo drop all other exports 71 | await writeFile(file, destination) 72 | return { method: method.toLowerCase(), route: getRoute(path), handler: destination } 73 | } 74 | const handlers = [] 75 | // @todo use Promise.all 76 | for (const method of SupportedMethods) { 77 | if (file.exports[method]) { 78 | const handler = await writeMethodHandler(method) 79 | handlers.push(handler) 80 | } 81 | } 82 | 83 | // Extract loaders in .generated/.loader 84 | if (file.exports.loader) { 85 | const route = getLoaderRoute(path) 86 | const destination = getLoaderDestination(path, baseDirectory) 87 | file.exports.default = file.exports.loader 88 | delete file.exports.loader 89 | await writeFile(file, destination) 90 | handlers.push({ method: "get", route, handler: destination }) 91 | } 92 | 93 | return handlers 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server Block Nuxt 2 | 3 | [![CI](https://github.com/Hebilicious/server-block-nuxt/actions/workflows/ci.yaml/badge.svg)](https://github.com/Hebilicious/server-block-nuxt/actions/workflows/ci.yaml) 4 | [![npm version][npm-version-src]][npm-version-href] 5 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 6 | [![License][license-src]][license-href] 7 | [![Nuxt][nuxt-src]][nuxt-href] 8 | 9 | [npm-version-src]: https://img.shields.io/npm/v/@hebilicious/server-block-nuxt/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 10 | [npm-version-href]: https://npmjs.com/package/@hebilicious/server-block-nuxt 11 | [npm-downloads-src]: https://img.shields.io/npm/dt/@hebilicious/server-block-nuxt.svg?style=flat&colorA=18181B&colorB=28CF8D 12 | [npm-downloads-href]: https://npmjs.com/package/@hebilicious/server-block-nuxt 13 | [license-src]: https://img.shields.io/npm/l/@hebilicious/server-block-nuxt.svg?style=flat&colorA=18181B&colorB=28CF8D 14 | [license-href]: https://npmjs.com/package/@hebilicious/server-block-nuxt 15 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 16 | [nuxt-href]: https://nuxt.com 17 | 18 | image 19 | 20 | ## 🚀 Welcome to __Server Block Nuxt__! 21 | 22 | _🧪 This module is experimental 🧪_ 23 | 24 | Nuxt Module that adds server block supports in your pages components. 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | ``` 32 | 33 | You can think of server block as a convenient way to write API handlers in your pages components. 34 | 35 | ## 📦 Install 36 | 37 | Install the module and the volar extension : 38 | 39 | ```bash 40 | npm i -D @hebilicious/server-block-nuxt @hebilicious/sfc-server-volar 41 | ``` 42 | 43 | Add the module to your Nuxt config : 44 | 45 | ```ts 46 | export default defineNuxtConfig({ 47 | modules: [ 48 | "@hebilicious/server-block-nuxt" 49 | ] 50 | }) 51 | ``` 52 | 53 | That's it ! 54 | The volar extension will be automatically installed by the nuxt module. 55 | 56 | ## 📖 Usage 57 | 58 | - *Server blocks are only available in pages components.* 59 | 60 | - *default exports are not available in server blocks. Use named exports.* 61 | 62 | Add a server block in a page component : 63 | 64 | ```html 65 | 66 | const message = "Hello World!!!" 67 | const bye = "bye!" 68 | export const GET = defineEventHandler(() =>({ message })) 69 | export const POST = defineEventHandler(() =>({ message: bye })) 70 | 71 | 72 | 75 | 76 | 79 | ``` 80 | 81 | This will generate 2 handlers in `server/.generated/api` : 82 | 83 | - GET : `server/.generated/api/message.get.ts` 84 | - POST : `server/.generated/api/message.post.ts` 85 | 86 | All HTTP methods are supported. 87 | 88 | ### Custom route 89 | 90 | You can override the default route convention with the `path` attribute : 91 | 92 | ```html 93 | 94 | export const GET = defineEventHandler((event) => { 95 | return "We're here now." 96 | }) 97 | 98 | 99 | 102 | 103 | 107 | ``` 108 | 109 | A `server/.generated/not-api/this/is/cool.get.ts` get handler will be generated. 110 | 111 | ## 💡 FAQ 112 | 113 | **Why `` and not `