@devlusoft/devix 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cli/build.js +145 -6
- package/dist/cli/build.js.map +4 -4
- package/dist/cli/dev.js +155 -4
- package/dist/cli/dev.js.map +4 -4
- package/dist/cli/generate.js +174 -12
- package/dist/cli/generate.js.map +4 -4
- package/dist/cli/index.js +200 -18
- package/dist/cli/index.js.map +4 -4
- package/dist/cli/start.js +12 -0
- package/dist/cli/start.js.map +3 -3
- package/dist/runtime/api-context.d.ts +3 -2
- package/dist/runtime/api-context.js.map +1 -1
- package/dist/runtime/fetch.d.ts +19 -0
- package/dist/runtime/fetch.js +35 -0
- package/dist/runtime/fetch.js.map +7 -0
- package/dist/runtime/index.d.ts +6 -1
- package/dist/runtime/index.js +68 -0
- package/dist/runtime/index.js.map +3 -3
- package/dist/server/api-router.d.ts +1 -0
- package/dist/server/api-router.js +1 -0
- package/dist/server/api-router.js.map +2 -2
- package/dist/server/api.js +1 -0
- package/dist/server/api.js.map +2 -2
- package/dist/server/index.js.map +2 -2
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.js +29 -0
- package/dist/utils/cookies.js.map +7 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/env.js +14 -0
- package/dist/utils/env.js.map +7 -0
- package/dist/utils/response.d.ts +3 -0
- package/dist/utils/response.js +10 -0
- package/dist/utils/response.js.map +7 -0
- package/dist/vite/codegen/extract-methods.d.ts +4 -0
- package/dist/vite/codegen/extract-methods.js +16 -0
- package/dist/vite/codegen/extract-methods.js.map +7 -0
- package/dist/vite/codegen/routes-dts.d.ts +10 -0
- package/dist/vite/codegen/routes-dts.js +61 -0
- package/dist/vite/codegen/routes-dts.js.map +7 -0
- package/dist/vite/codegen/scan-api.d.ts +2 -0
- package/dist/vite/codegen/scan-api.js +78 -0
- package/dist/vite/codegen/scan-api.js.map +7 -0
- package/dist/vite/codegen/write-routes-dts.d.ts +1 -0
- package/dist/vite/codegen/write-routes-dts.js +17 -0
- package/dist/vite/codegen/write-routes-dts.js.map +7 -0
- package/dist/vite/index.js +143 -4
- package/dist/vite/index.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,16 +137,18 @@ export default function RootLayout({ children }: LayoutProps) {
|
|
|
137
137
|
### Rutas API
|
|
138
138
|
|
|
139
139
|
```ts
|
|
140
|
-
import type
|
|
140
|
+
import { json, type RouteHandler } from '@devlusoft/devix'
|
|
141
141
|
|
|
142
|
-
export const GET: RouteHandler = async (ctx
|
|
143
|
-
return
|
|
142
|
+
export const GET: RouteHandler = async (ctx) => {
|
|
143
|
+
return { hello: 'world' } // auto JSON 200
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
export const POST: RouteHandler = async (ctx, req) => {
|
|
147
147
|
const body = await req.json()
|
|
148
|
-
return
|
|
148
|
+
return json(body, 201)
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
export const DELETE: RouteHandler = async () => null // 204
|
|
150
152
|
```
|
|
151
153
|
|
|
152
154
|
### Generación estática (SSG)
|
package/dist/cli/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/cli/build.ts
|
|
2
|
-
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
3
3
|
import { resolve as resolve2 } from "node:path";
|
|
4
4
|
import { build } from "vite";
|
|
5
5
|
|
|
@@ -151,6 +151,11 @@ export function handleApiRequest(url, request) {
|
|
|
151
151
|
`;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// src/utils/patterns.ts
|
|
155
|
+
function routePattern(rel) {
|
|
156
|
+
return rel.replace(/\.(tsx|ts|jsx|js)$/, "").replace(/\(.*?\)\//g, "").replace(/^index$|\/index$/, "").replace(/\[([^\]]+)]/g, ":$1") || "/";
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
// src/server/pages-router.ts
|
|
155
160
|
var cache = null;
|
|
156
161
|
function invalidatePagesCache() {
|
|
@@ -158,6 +163,11 @@ function invalidatePagesCache() {
|
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
// src/server/api-router.ts
|
|
166
|
+
function keyToRoutePattern(key, apiDir) {
|
|
167
|
+
const rel = key.slice(apiDir.length + 1).replace(/\\/g, "/");
|
|
168
|
+
const pattern = routePattern(rel);
|
|
169
|
+
return pattern === "/" ? "/api" : `/api/${pattern}`.replace("/api//", "/api/");
|
|
170
|
+
}
|
|
161
171
|
var cache2 = null;
|
|
162
172
|
function invalidateApiCache() {
|
|
163
173
|
cache2 = null;
|
|
@@ -170,6 +180,114 @@ export {RouterContext} from '@devlusoft/devix/runtime/context'
|
|
|
170
180
|
`;
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
// src/vite/codegen/scan-api.ts
|
|
184
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
185
|
+
import { join, relative } from "node:path";
|
|
186
|
+
|
|
187
|
+
// src/vite/codegen/extract-methods.ts
|
|
188
|
+
var METHOD_EXPORT_RE = /export\s+(?:const|async\s+function|function)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
|
|
189
|
+
function stripComments(content) {
|
|
190
|
+
return content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
191
|
+
}
|
|
192
|
+
function extractHttpMethods(content) {
|
|
193
|
+
const found = /* @__PURE__ */ new Set();
|
|
194
|
+
for (const match of stripComments(content).matchAll(METHOD_EXPORT_RE)) {
|
|
195
|
+
found.add(match[1]);
|
|
196
|
+
}
|
|
197
|
+
return [...found];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/vite/codegen/routes-dts.ts
|
|
201
|
+
function filePathToIdentifier(filePath, apiDir) {
|
|
202
|
+
return "_api_" + filePath.slice(`${apiDir}/`.length).replace(/\.(ts|tsx)$/, "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
203
|
+
}
|
|
204
|
+
function buildRouteEntry(filePath, apiDir, methods) {
|
|
205
|
+
return {
|
|
206
|
+
filePath,
|
|
207
|
+
urlPattern: keyToRoutePattern(filePath, apiDir),
|
|
208
|
+
identifier: filePathToIdentifier(filePath, apiDir),
|
|
209
|
+
methods
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function generateRoutesDts(entries, apiDir) {
|
|
213
|
+
if (entries.length === 0) {
|
|
214
|
+
return `// auto-generado por devix \u2014 no editar
|
|
215
|
+
declare module '@devlusoft/devix' {
|
|
216
|
+
interface ApiRoutes {}
|
|
217
|
+
}
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
const imports = entries.map((e) => {
|
|
221
|
+
const importPath = "../" + e.filePath.replace(/\.(ts|tsx)$/, "");
|
|
222
|
+
return `import type * as ${e.identifier} from '${importPath}'`;
|
|
223
|
+
}).join("\n");
|
|
224
|
+
const routeLines = entries.flatMap(
|
|
225
|
+
(e) => e.methods.map(
|
|
226
|
+
(m) => ` '${m} ${e.urlPattern}': InferRoute<(typeof ${e.identifier})['${m}']>`
|
|
227
|
+
)
|
|
228
|
+
).join("\n");
|
|
229
|
+
return `// auto-generado por devix \u2014 no editar
|
|
230
|
+
${imports}
|
|
231
|
+
|
|
232
|
+
type InferRoute<T> = T extends (...args: any[]) => any
|
|
233
|
+
? Exclude<Awaited<ReturnType<T>>, Response | null | void>
|
|
234
|
+
: never
|
|
235
|
+
|
|
236
|
+
declare module '@devlusoft/devix' {
|
|
237
|
+
interface ApiRoutes {
|
|
238
|
+
${routeLines}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/vite/codegen/scan-api.ts
|
|
245
|
+
function walkDir(dir, root) {
|
|
246
|
+
const entries = [];
|
|
247
|
+
for (const name of readdirSync(dir)) {
|
|
248
|
+
const full = join(dir, name);
|
|
249
|
+
if (statSync(full).isDirectory()) {
|
|
250
|
+
entries.push(...walkDir(full, root));
|
|
251
|
+
} else if (/\.(ts|tsx)$/.test(name)) {
|
|
252
|
+
entries.push(relative(root, full).replace(/\\/g, "/"));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return entries;
|
|
256
|
+
}
|
|
257
|
+
function scanApiFiles(appDir, projectRoot) {
|
|
258
|
+
const apiDir = join(projectRoot, appDir, "api");
|
|
259
|
+
let files;
|
|
260
|
+
try {
|
|
261
|
+
files = walkDir(apiDir, projectRoot);
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
return files.filter((f) => !f.endsWith("middleware.ts") && !f.endsWith("middleware.tsx")).flatMap((filePath) => {
|
|
266
|
+
try {
|
|
267
|
+
const content = readFileSync(join(projectRoot, filePath), "utf-8");
|
|
268
|
+
const methods = extractHttpMethods(content);
|
|
269
|
+
if (methods.length === 0) return [];
|
|
270
|
+
return [buildRouteEntry(filePath, `${appDir}/api`, methods)];
|
|
271
|
+
} catch {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/vite/codegen/write-routes-dts.ts
|
|
278
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync } from "node:fs";
|
|
279
|
+
import { join as join2 } from "node:path";
|
|
280
|
+
function writeRoutesDts(content, projectRoot) {
|
|
281
|
+
const devixDir = join2(projectRoot, ".devix");
|
|
282
|
+
const outPath = join2(devixDir, "routes.d.ts");
|
|
283
|
+
mkdirSync(devixDir, { recursive: true });
|
|
284
|
+
if (existsSync(outPath) && readFileSync2(outPath, "utf-8") === content) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
writeFileSync(outPath, content, "utf-8");
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
173
291
|
// src/vite/index.ts
|
|
174
292
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
175
293
|
var VIRTUAL_ENTRY_CLIENT = "virtual:devix/entry-client";
|
|
@@ -206,14 +324,35 @@ function devix(config2) {
|
|
|
206
324
|
if (id === `\0${VIRTUAL_CONTEXT}`)
|
|
207
325
|
return generateContext();
|
|
208
326
|
},
|
|
327
|
+
buildStart() {
|
|
328
|
+
const root = process.cwd();
|
|
329
|
+
const entries = scanApiFiles(appDir, root);
|
|
330
|
+
writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root);
|
|
331
|
+
},
|
|
209
332
|
configureServer(server) {
|
|
333
|
+
const root = process.cwd();
|
|
334
|
+
const regenerateDts = () => {
|
|
335
|
+
const entries = scanApiFiles(appDir, root);
|
|
336
|
+
writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root);
|
|
337
|
+
};
|
|
210
338
|
server.watcher.on("add", (file) => {
|
|
211
|
-
if (file.startsWith(resolve(
|
|
212
|
-
if (file.includes(`${appDir}/api`))
|
|
339
|
+
if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache();
|
|
340
|
+
if (file.includes(`${appDir}/api`)) {
|
|
341
|
+
invalidateApiCache();
|
|
342
|
+
regenerateDts();
|
|
343
|
+
}
|
|
213
344
|
});
|
|
214
345
|
server.watcher.on("unlink", (file) => {
|
|
215
|
-
if (file.startsWith(resolve(
|
|
216
|
-
if (file.includes(`${appDir}/api`))
|
|
346
|
+
if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache();
|
|
347
|
+
if (file.includes(`${appDir}/api`)) {
|
|
348
|
+
invalidateApiCache();
|
|
349
|
+
regenerateDts();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
server.watcher.on("change", (file) => {
|
|
353
|
+
if (file.includes(`${appDir}/api`) && !file.endsWith("middleware.ts")) {
|
|
354
|
+
regenerateDts();
|
|
355
|
+
}
|
|
217
356
|
});
|
|
218
357
|
}
|
|
219
358
|
};
|
|
@@ -278,7 +417,7 @@ var runtimeConfig = {
|
|
|
278
417
|
loaderTimeout: parseDuration(config.loaderTimeout ?? 1e4),
|
|
279
418
|
output: config.output ?? "server"
|
|
280
419
|
};
|
|
281
|
-
|
|
420
|
+
writeFileSync2(
|
|
282
421
|
resolve2(process.cwd(), "dist/devix.config.json"),
|
|
283
422
|
JSON.stringify(runtimeConfig, null, 2),
|
|
284
423
|
"utf-8"
|
package/dist/cli/build.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/cli/build.ts", "../../src/vite/index.ts", "../../src/vite/codegen/entry-client.ts", "../../src/vite/codegen/client-routes.ts", "../../src/vite/codegen/render.ts", "../../src/vite/codegen/api.ts", "../../src/server/pages-router.ts", "../../src/server/api-router.ts", "../../src/vite/codegen/context.ts", "../../src/utils/duration.ts"],
|
|
4
|
-
"sourcesContent": ["import {writeFileSync} from 'node:fs'\nimport {resolve} from 'node:path'\nimport {build} from 'vite'\nimport type {DevixConfig} from '../config'\nimport {devix} from '../vite'\nimport {parseDuration} from '../utils/duration'\n\nconst config: DevixConfig = (await import(`${process.cwd()}/devix.config.ts`)).default\nconst baseConfig = devix(config)\n\nawait build({\n ...baseConfig,\n configFile: false,\n build: {\n outDir: 'dist/client',\n manifest: true,\n rolldownOptions: {\n input: 'virtual:devix/entry-client',\n },\n },\n})\n\nawait build({\n ...baseConfig,\n configFile: false,\n build: {\n ssr: true,\n outDir: 'dist/server',\n rolldownOptions: {\n input: {\n render: 'virtual:devix/render',\n api: 'virtual:devix/api',\n },\n },\n },\n})\n\nconst runtimeConfig = {\n port: config.port ?? 3000,\n host: config.host ?? false,\n loaderTimeout: parseDuration(config.loaderTimeout ?? 10_000),\n output: config.output ?? 'server',\n}\n\nwriteFileSync(\n resolve(process.cwd(), 'dist/devix.config.json'),\n JSON.stringify(runtimeConfig, null, 2),\n 'utf-8'\n)\n\n\nexport {}", "import {UserConfig, Plugin, mergeConfig} from 'vite'\nimport type {DevixConfig} from '../config'\nimport react from '@vitejs/plugin-react'\nimport {fileURLToPath} from 'node:url'\nimport {dirname, resolve} from 'node:path'\nimport {generateEntryClient} from './codegen/entry-client'\nimport {generateClientRoutes} from './codegen/client-routes'\nimport {generateRender} from './codegen/render'\nimport {generateApi} from './codegen/api'\nimport {invalidatePagesCache} from \"../server/pages-router\";\nimport {invalidateApiCache} from \"../server/api-router\";\nimport {generateContext} from \"./codegen/context\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nconst VIRTUAL_ENTRY_CLIENT = 'virtual:devix/entry-client'\nconst VIRTUAL_CLIENT_ROUTES = 'virtual:devix/client-routes'\nconst VIRTUAL_RENDER = 'virtual:devix/render'\nconst VIRTUAL_API = 'virtual:devix/api'\nconst VIRTUAL_CONTEXT = 'virtual:devix/context'\n\nexport function devix(config: DevixConfig): UserConfig {\n const appDir = config.appDir ?? 'app'\n const pagesDir = `${appDir}/pages`\n const cssUrls = (config.css ?? []).map(u => u.startsWith('/') ? u : `/${u.replace(/^\\.\\//, '')}`)\n\n const renderPath = resolve(__dirname, '../server/render.js').replace(/\\\\/g, '/')\n const apiPath = resolve(__dirname, '../server/api.js').replace(/\\\\/g, '/')\n const matcherPath = resolve(__dirname, '../runtime/client-router.js').replace(/\\\\/g, '/')\n\n const virtualPlugin: Plugin = {\n name: 'devix',\n enforce: 'pre',\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_CLIENT) return `\\0${VIRTUAL_ENTRY_CLIENT}`\n if (id === VIRTUAL_CLIENT_ROUTES) return `\\0${VIRTUAL_CLIENT_ROUTES}`\n if (id === VIRTUAL_RENDER) return `\\0${VIRTUAL_RENDER}`\n if (id === VIRTUAL_API) return `\\0${VIRTUAL_API}`\n if (id === VIRTUAL_CONTEXT) return `\\0${VIRTUAL_CONTEXT}`\n },\n\n load(id) {\n if (id === `\\0${VIRTUAL_ENTRY_CLIENT}`)\n return generateEntryClient({cssUrls})\n if (id === `\\0${VIRTUAL_CLIENT_ROUTES}`)\n return generateClientRoutes({pagesDir, matcherPath})\n if (id === `\\0${VIRTUAL_RENDER}`)\n return generateRender({pagesDir, renderPath})\n if (id === `\\0${VIRTUAL_API}`)\n return generateApi({apiPath, appDir})\n if (id === `\\0${VIRTUAL_CONTEXT}`)\n return generateContext()\n },\n\n configureServer(server) {\n server.watcher.on('add', (file) => {\n if (file.startsWith(resolve(process.cwd(), pagesDir))) invalidatePagesCache()\n if (file.includes(`${appDir}/api`)) invalidateApiCache()\n })\n server.watcher.on('unlink', (file) => {\n if (file.startsWith(resolve(process.cwd(), pagesDir))) invalidatePagesCache()\n if (file.includes(`${appDir}/api`)) invalidateApiCache()\n })\n },\n }\n\n const base: UserConfig = {\n plugins: [react(), virtualPlugin],\n ssr: {noExternal: ['@devlusoft/devix']},\n ...(config.envPrefix ? {envPrefix: config.envPrefix} : {}),\n }\n\n return mergeConfig(base, config.vite ?? {})\n}", "interface EntryClientOptions {\n cssUrls: string[]\n}\n\nexport function generateEntryClient({cssUrls}: EntryClientOptions): string {\n const cssImports = cssUrls.map(u => `import '${u}'`).join('\\n')\n\n return `\n${cssImports}\nimport \"@vitejs/plugin-react/preamble\"\nimport React from \"react\"\nimport {hydrateRoot, createRoot} from 'react-dom/client'\nimport {matchClientRoute, loadErrorPage, getDefaultErrorPage} from 'virtual:devix/client-routes'\nimport {RouterProvider} from '@devlusoft/devix'\n\nconst root = document.getElementById('devix-root')\n\nif (!window.__DEVIX__) {\n const ErrorPage = getDefaultErrorPage()\n createRoot(root).render(React.createElement(ErrorPage, {statusCode: 500, message: 'Server error'}))\n} else {\n const {metadata, viewport, clientEntry} = window.__DEVIX__\n const loaderData = window.__LOADER_DATA__\n const layoutsData = window.__LAYOUTS_DATA__ ?? []\n\n const matched = matchClientRoute(window.location.pathname)\n\n if (matched) {\n const [pageMod, ...layoutMods] = await Promise.all([\n matched.load(),\n ...matched.loadLayouts.map(l => l()),\n ])\n hydrateRoot(\n root,\n React.createElement(RouterProvider, {\n clientEntry,\n initialData: loaderData,\n initialParams: matched.params,\n initialPage: pageMod.default,\n initialLayouts: layoutMods.map(m => m.default),\n initialLayoutsData: layoutsData,\n initialMeta: metadata,\n initialViewport: viewport,\n })\n )\n } else {\n const ErrorPage = await loadErrorPage() ?? getDefaultErrorPage()\n createRoot(root).render(\n React.createElement(RouterProvider, {\n clientEntry,\n initialData: null,\n initialParams: {},\n initialPage: () => null,\n initialLayouts: [],\n initialLayoutsData: [],\n initialMeta: null,\n initialError: {statusCode: 404, message: 'Not found'},\n initialErrorPage: ErrorPage,\n })\n )\n }\n}\n`\n}", "interface ClientRoutesOptions {\n pagesDir: string\n matcherPath: string\n}\n\nexport function generateClientRoutes({pagesDir, matcherPath}: ClientRoutesOptions) {\n return `\nimport React from 'react'\nimport { createMatcher } from '${matcherPath}'\nconst pageFiles = import.meta.glob(['/${pagesDir}/**/*.tsx', '!**/error.tsx', '!**/layout.tsx'])\nconst layoutFiles = import.meta.glob('/${pagesDir}/**/layout.tsx')\nconst errorFiles = import.meta.glob('/${pagesDir}/**/error.tsx')\n\nexport const matchClientRoute = createMatcher(pageFiles, layoutFiles)\n\nexport async function loadErrorPage() {\n const key = Object.keys(errorFiles)[0]\n if (!key) return null\n const mod = await errorFiles[key]()\n return mod?.default ?? null\n}\n\nexport function getDefaultErrorPage() {\n return function DefaultError({ statusCode, message }) {\n return React.createElement('main', {\n style: { minHeight: '100dvh', display: 'flex', flexDirection: 'column', \n alignItems: 'center', justifyContent: 'center', gap: '8px',\n fontFamily: 'system-ui, sans-serif' }\n },\n React.createElement('h1', {style: {fontSize: '4rem', fontWeight: 700}}, statusCode),\n React.createElement('p', {style: {color: '#666'}}, message ?? 'An unexpected error occurred'),\n )\n }\n}\n`\n}", "interface RenderOptions {\n pagesDir: string\n renderPath: string\n}\n\nexport function generateRender({pagesDir, renderPath}: RenderOptions): string {\n return `\nimport { render as _render, runLoader as _runLoader, getStaticRoutes as _getStaticRoutes } from '${renderPath}'\n\nconst _pages = import.meta.glob(['/${pagesDir}/**/*.tsx', '!**/error.tsx', '!**/layout.tsx'])\nconst _layouts = import.meta.glob('/${pagesDir}/**/layout.tsx')\n\nconst _glob = {\n pages: _pages,\n layouts: _layouts,\n pagesDir: '/${pagesDir}',\n}\n\nexport function render(url, request, options) {\n return _render(url, request, _glob, options)\n}\n\nexport function runLoader(url, request, options) {\n return _runLoader(url, request, _glob, options)\n}\n\nexport function getStaticRoutes() {\n return _getStaticRoutes(_glob)\n}\n`\n}\n", "interface ApiOptions {\n apiPath: string\n appDir: string\n}\n\nexport function generateApi({apiPath, appDir}: ApiOptions): string {\n return `\nimport { handleApiRequest as _handleApiRequest } from '${apiPath}'\n\nconst _routes = import.meta.glob(['/${appDir}/api/**/*.ts', '!**/middleware.ts'])\nconst _middlewares = import.meta.glob('/${appDir}/api/**/middleware.ts')\n\nconst _glob = {\n routes: _routes,\n middlewares: _middlewares,\n apiDir: '/${appDir}/api',\n}\n\nexport function handleApiRequest(url, request) {\n return _handleApiRequest(url, request, _glob)\n}\n`\n}\n", "import {routePattern} from \"../utils/patterns\";\n\nexport interface Page {\n path: string\n key: string\n params: string[]\n regex: RegExp\n}\n\nexport interface Layout {\n dir: string\n key: string\n}\n\nexport interface PagesResult {\n pages: Page[]\n layouts: Layout[]\n}\n\nfunction keyToRoutePattern(key: string, pagesDir: string): string {\n const rel = key.slice(pagesDir.length + 1).replace(/\\\\/g, '/')\n const pattern = routePattern(rel)\n return pattern === \"/\" ? \"/\" : `/${pattern}`\n}\n\nfunction keyToDir(key: string): string {\n return key.slice(0, key.lastIndexOf('/'))\n}\n\nlet cache: PagesResult | null = null\n\nexport function invalidatePagesCache() {\n cache = null\n}\n\nexport function buildPages(pageKeys: string[], layoutKeys: string[], pagesDir: string): PagesResult {\n if (cache) return cache\n\n const pages: Page[] = []\n const layouts: Layout[] = []\n\n for (const key of layoutKeys) {\n layouts.push({dir: keyToDir(key), key})\n }\n\n for (const key of pageKeys) {\n const pattern = keyToRoutePattern(key, pagesDir)\n const params = [...pattern.matchAll(/:([^/]+)/g)].map(m => m[1])\n const regexStr = pattern\n .replace(/:[^/]+/g, '([^/]+)')\n .replace(/\\//g, '\\\\/')\n pages.push({path: pattern, key, params, regex: new RegExp(`^${regexStr}$`)})\n }\n\n pages.sort((a, b) => {\n const aScore = (a.path.match(/:/g) || []).length\n const bScore = (b.path.match(/:/g) || []).length\n if (aScore !== bScore) return aScore - bScore\n return b.path.length - a.path.length\n })\n\n cache = {pages, layouts}\n return cache\n}\n\nexport function collectLayoutChain(pageKey: string, layouts: Layout[]): Layout[] {\n const pageDir = keyToDir(pageKey)\n\n return layouts\n .filter(layout => pageDir.startsWith(layout.dir))\n .sort((a, b) => a.dir.split('/').length - b.dir.split('/').length)\n}\n\nexport function matchPage(pathname: string, pages: Page[]): {\n page: Page\n params: Record<string, string>\n} | null {\n for (const page of pages) {\n const match = pathname.match(page.regex)\n if (match) {\n const params: Record<string, string> = {}\n page.params.forEach((name, i) => {\n params[name] = decodeURIComponent(match[i + 1])\n })\n return {page, params}\n }\n }\n return null\n}\n", "import {routePattern} from \"../utils/patterns\";\n\nexport interface ApiRoute {\n path: string\n key: string\n params: string[]\n regex: RegExp\n}\n\nexport interface ApiMiddleware {\n dir: string\n key: string\n}\n\nexport interface ApiResult {\n routes: ApiRoute[]\n middlewares: ApiMiddleware[]\n}\n\nfunction keyToRoutePattern(key: string, apiDir: string): string {\n const rel = key.slice(apiDir.length + 1).replace(/\\\\/g, '/')\n const pattern = routePattern(rel)\n return pattern === '/' ? '/api' : `/api/${pattern}`.replace('/api//', '/api/')\n}\n\nfunction keyToDir(key: string): string {\n return key.slice(0, key.lastIndexOf('/'))\n}\n\nlet cache: ApiResult | null = null\n\nexport function invalidateApiCache() {\n cache = null\n}\n\nexport function buildRoutes(routeKeys: string[], middlewareKeys: string[], apiDir: string): ApiResult {\n if (cache) return cache\n\n const routes: ApiRoute[] = []\n const middlewares: ApiMiddleware[] = []\n\n for (const key of middlewareKeys) {\n middlewares.push({dir: keyToDir(key), key})\n }\n\n for (const key of routeKeys) {\n const pattern = keyToRoutePattern(key, apiDir)\n const params = [...pattern.matchAll(/:([^/]+)/g)].map(m => m[1])\n const regexStr = pattern\n .replace(/:[^/]+/g, '([^/]+)')\n .replace(/\\//g, '\\\\/')\n routes.push({path: pattern, key, params, regex: new RegExp(`^${regexStr}$`)})\n }\n routes.sort((a, b) => {\n const aScore = (a.path.match(/:/g) || []).length\n const bScore = (b.path.match(/:/g) || []).length\n if (aScore !== bScore) return aScore - bScore\n return b.path.length - a.path.length\n })\n\n cache = {routes, middlewares}\n return cache\n}\n\nexport function collectMiddlewareChain(routeKey: string, middlewares: ApiMiddleware[]): ApiMiddleware[] {\n const routeDir = keyToDir(routeKey)\n\n return middlewares\n .filter(mw => routeDir.startsWith(mw.dir))\n .sort((a, b) => a.dir.split('/').length - b.dir.split('/').length)\n}\n\nexport function matchRoute(\n pathname: string,\n routes: ApiRoute[]\n): {route: ApiRoute; params: Record<string, string>} | null {\n for (const route of routes) {\n const match = pathname.match(route.regex)\n if (match) {\n const params: Record<string, string> = {}\n route.params.forEach((name, i) => {\n params[name] = decodeURIComponent(match[i + 1])\n })\n return {route, params}\n }\n }\n return null\n}\n", "export function generateContext(): string {\n return `\nexport {RouterContext} from '@devlusoft/devix/runtime/context'\n`\n}", "export function parseDuration(value: number | string): number {\n if (typeof value === 'number') return value\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h)?$/)\n if (!match) throw new Error(`[devix] Invalid duration: \"${value}\". Use a number (ms) or a string like \"5s\", \"2m\", \"500ms\".`)\n const n = parseFloat(match[1])\n switch (match[2]) {\n case 'h': return n * 3_600_000\n case 'm': return n * 60_000\n case 's': return n * 1_000\n case 'ms':\n default: return n\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,SAAQ,
|
|
6
|
-
"names": ["resolve", "cache", "config", "resolve"]
|
|
3
|
+
"sources": ["../../src/cli/build.ts", "../../src/vite/index.ts", "../../src/vite/codegen/entry-client.ts", "../../src/vite/codegen/client-routes.ts", "../../src/vite/codegen/render.ts", "../../src/vite/codegen/api.ts", "../../src/utils/patterns.ts", "../../src/server/pages-router.ts", "../../src/server/api-router.ts", "../../src/vite/codegen/context.ts", "../../src/vite/codegen/scan-api.ts", "../../src/vite/codegen/extract-methods.ts", "../../src/vite/codegen/routes-dts.ts", "../../src/vite/codegen/write-routes-dts.ts", "../../src/utils/duration.ts"],
|
|
4
|
+
"sourcesContent": ["import {writeFileSync} from 'node:fs'\nimport {resolve} from 'node:path'\nimport {build} from 'vite'\nimport type {DevixConfig} from '../config'\nimport {devix} from '../vite'\nimport {parseDuration} from '../utils/duration'\n\nconst config: DevixConfig = (await import(`${process.cwd()}/devix.config.ts`)).default\nconst baseConfig = devix(config)\n\nawait build({\n ...baseConfig,\n configFile: false,\n build: {\n outDir: 'dist/client',\n manifest: true,\n rolldownOptions: {\n input: 'virtual:devix/entry-client',\n },\n },\n})\n\nawait build({\n ...baseConfig,\n configFile: false,\n build: {\n ssr: true,\n outDir: 'dist/server',\n rolldownOptions: {\n input: {\n render: 'virtual:devix/render',\n api: 'virtual:devix/api',\n },\n },\n },\n})\n\nconst runtimeConfig = {\n port: config.port ?? 3000,\n host: config.host ?? false,\n loaderTimeout: parseDuration(config.loaderTimeout ?? 10_000),\n output: config.output ?? 'server',\n}\n\nwriteFileSync(\n resolve(process.cwd(), 'dist/devix.config.json'),\n JSON.stringify(runtimeConfig, null, 2),\n 'utf-8'\n)\n\n\nexport {}", "import {UserConfig, Plugin, mergeConfig} from 'vite'\nimport type {DevixConfig} from '../config'\nimport react from '@vitejs/plugin-react'\nimport {fileURLToPath} from 'node:url'\nimport {dirname, resolve} from 'node:path'\nimport {generateEntryClient} from './codegen/entry-client'\nimport {generateClientRoutes} from './codegen/client-routes'\nimport {generateRender} from './codegen/render'\nimport {generateApi} from './codegen/api'\nimport {invalidatePagesCache} from \"../server/pages-router\";\nimport {invalidateApiCache} from \"../server/api-router\";\nimport {generateContext} from \"./codegen/context\";\nimport {scanApiFiles} from \"./codegen/scan-api\";\nimport {generateRoutesDts} from \"./codegen/routes-dts\";\nimport {writeRoutesDts} from \"./codegen/write-routes-dts\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nconst VIRTUAL_ENTRY_CLIENT = 'virtual:devix/entry-client'\nconst VIRTUAL_CLIENT_ROUTES = 'virtual:devix/client-routes'\nconst VIRTUAL_RENDER = 'virtual:devix/render'\nconst VIRTUAL_API = 'virtual:devix/api'\nconst VIRTUAL_CONTEXT = 'virtual:devix/context'\n\nexport function devix(config: DevixConfig): UserConfig {\n const appDir = config.appDir ?? 'app'\n const pagesDir = `${appDir}/pages`\n const cssUrls = (config.css ?? []).map(u => u.startsWith('/') ? u : `/${u.replace(/^\\.\\//, '')}`)\n\n const renderPath = resolve(__dirname, '../server/render.js').replace(/\\\\/g, '/')\n const apiPath = resolve(__dirname, '../server/api.js').replace(/\\\\/g, '/')\n const matcherPath = resolve(__dirname, '../runtime/client-router.js').replace(/\\\\/g, '/')\n\n const virtualPlugin: Plugin = {\n name: 'devix',\n enforce: 'pre',\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_CLIENT) return `\\0${VIRTUAL_ENTRY_CLIENT}`\n if (id === VIRTUAL_CLIENT_ROUTES) return `\\0${VIRTUAL_CLIENT_ROUTES}`\n if (id === VIRTUAL_RENDER) return `\\0${VIRTUAL_RENDER}`\n if (id === VIRTUAL_API) return `\\0${VIRTUAL_API}`\n if (id === VIRTUAL_CONTEXT) return `\\0${VIRTUAL_CONTEXT}`\n },\n\n load(id) {\n if (id === `\\0${VIRTUAL_ENTRY_CLIENT}`)\n return generateEntryClient({cssUrls})\n if (id === `\\0${VIRTUAL_CLIENT_ROUTES}`)\n return generateClientRoutes({pagesDir, matcherPath})\n if (id === `\\0${VIRTUAL_RENDER}`)\n return generateRender({pagesDir, renderPath})\n if (id === `\\0${VIRTUAL_API}`)\n return generateApi({apiPath, appDir})\n if (id === `\\0${VIRTUAL_CONTEXT}`)\n return generateContext()\n },\n\n buildStart() {\n const root = process.cwd()\n const entries = scanApiFiles(appDir, root)\n writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root)\n },\n\n configureServer(server) {\n const root = process.cwd()\n\n const regenerateDts = () => {\n const entries = scanApiFiles(appDir, root)\n writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root)\n }\n\n server.watcher.on('add', (file) => {\n if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache()\n if (file.includes(`${appDir}/api`)) { invalidateApiCache(); regenerateDts() }\n })\n server.watcher.on('unlink', (file) => {\n if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache()\n if (file.includes(`${appDir}/api`)) { invalidateApiCache(); regenerateDts() }\n })\n server.watcher.on('change', (file) => {\n if (file.includes(`${appDir}/api`) && !file.endsWith('middleware.ts')) {\n regenerateDts()\n }\n })\n },\n }\n\n const base: UserConfig = {\n plugins: [react(), virtualPlugin],\n ssr: {noExternal: ['@devlusoft/devix']},\n ...(config.envPrefix ? {envPrefix: config.envPrefix} : {}),\n }\n\n return mergeConfig(base, config.vite ?? {})\n}", "interface EntryClientOptions {\n cssUrls: string[]\n}\n\nexport function generateEntryClient({cssUrls}: EntryClientOptions): string {\n const cssImports = cssUrls.map(u => `import '${u}'`).join('\\n')\n\n return `\n${cssImports}\nimport \"@vitejs/plugin-react/preamble\"\nimport React from \"react\"\nimport {hydrateRoot, createRoot} from 'react-dom/client'\nimport {matchClientRoute, loadErrorPage, getDefaultErrorPage} from 'virtual:devix/client-routes'\nimport {RouterProvider} from '@devlusoft/devix'\n\nconst root = document.getElementById('devix-root')\n\nif (!window.__DEVIX__) {\n const ErrorPage = getDefaultErrorPage()\n createRoot(root).render(React.createElement(ErrorPage, {statusCode: 500, message: 'Server error'}))\n} else {\n const {metadata, viewport, clientEntry} = window.__DEVIX__\n const loaderData = window.__LOADER_DATA__\n const layoutsData = window.__LAYOUTS_DATA__ ?? []\n\n const matched = matchClientRoute(window.location.pathname)\n\n if (matched) {\n const [pageMod, ...layoutMods] = await Promise.all([\n matched.load(),\n ...matched.loadLayouts.map(l => l()),\n ])\n hydrateRoot(\n root,\n React.createElement(RouterProvider, {\n clientEntry,\n initialData: loaderData,\n initialParams: matched.params,\n initialPage: pageMod.default,\n initialLayouts: layoutMods.map(m => m.default),\n initialLayoutsData: layoutsData,\n initialMeta: metadata,\n initialViewport: viewport,\n })\n )\n } else {\n const ErrorPage = await loadErrorPage() ?? getDefaultErrorPage()\n createRoot(root).render(\n React.createElement(RouterProvider, {\n clientEntry,\n initialData: null,\n initialParams: {},\n initialPage: () => null,\n initialLayouts: [],\n initialLayoutsData: [],\n initialMeta: null,\n initialError: {statusCode: 404, message: 'Not found'},\n initialErrorPage: ErrorPage,\n })\n )\n }\n}\n`\n}", "interface ClientRoutesOptions {\n pagesDir: string\n matcherPath: string\n}\n\nexport function generateClientRoutes({pagesDir, matcherPath}: ClientRoutesOptions) {\n return `\nimport React from 'react'\nimport { createMatcher } from '${matcherPath}'\nconst pageFiles = import.meta.glob(['/${pagesDir}/**/*.tsx', '!**/error.tsx', '!**/layout.tsx'])\nconst layoutFiles = import.meta.glob('/${pagesDir}/**/layout.tsx')\nconst errorFiles = import.meta.glob('/${pagesDir}/**/error.tsx')\n\nexport const matchClientRoute = createMatcher(pageFiles, layoutFiles)\n\nexport async function loadErrorPage() {\n const key = Object.keys(errorFiles)[0]\n if (!key) return null\n const mod = await errorFiles[key]()\n return mod?.default ?? null\n}\n\nexport function getDefaultErrorPage() {\n return function DefaultError({ statusCode, message }) {\n return React.createElement('main', {\n style: { minHeight: '100dvh', display: 'flex', flexDirection: 'column', \n alignItems: 'center', justifyContent: 'center', gap: '8px',\n fontFamily: 'system-ui, sans-serif' }\n },\n React.createElement('h1', {style: {fontSize: '4rem', fontWeight: 700}}, statusCode),\n React.createElement('p', {style: {color: '#666'}}, message ?? 'An unexpected error occurred'),\n )\n }\n}\n`\n}", "interface RenderOptions {\n pagesDir: string\n renderPath: string\n}\n\nexport function generateRender({pagesDir, renderPath}: RenderOptions): string {\n return `\nimport { render as _render, runLoader as _runLoader, getStaticRoutes as _getStaticRoutes } from '${renderPath}'\n\nconst _pages = import.meta.glob(['/${pagesDir}/**/*.tsx', '!**/error.tsx', '!**/layout.tsx'])\nconst _layouts = import.meta.glob('/${pagesDir}/**/layout.tsx')\n\nconst _glob = {\n pages: _pages,\n layouts: _layouts,\n pagesDir: '/${pagesDir}',\n}\n\nexport function render(url, request, options) {\n return _render(url, request, _glob, options)\n}\n\nexport function runLoader(url, request, options) {\n return _runLoader(url, request, _glob, options)\n}\n\nexport function getStaticRoutes() {\n return _getStaticRoutes(_glob)\n}\n`\n}\n", "interface ApiOptions {\n apiPath: string\n appDir: string\n}\n\nexport function generateApi({apiPath, appDir}: ApiOptions): string {\n return `\nimport { handleApiRequest as _handleApiRequest } from '${apiPath}'\n\nconst _routes = import.meta.glob(['/${appDir}/api/**/*.ts', '!**/middleware.ts'])\nconst _middlewares = import.meta.glob('/${appDir}/api/**/middleware.ts')\n\nconst _glob = {\n routes: _routes,\n middlewares: _middlewares,\n apiDir: '/${appDir}/api',\n}\n\nexport function handleApiRequest(url, request) {\n return _handleApiRequest(url, request, _glob)\n}\n`\n}\n", "export function routePattern(rel: string): string {\n return rel\n .replace(/\\.(tsx|ts|jsx|js)$/, '')\n .replace(/\\(.*?\\)\\//g, '')\n .replace(/^index$|\\/index$/, '')\n .replace(/\\[([^\\]]+)]/g, ':$1')\n || '/'\n}", "import {routePattern} from \"../utils/patterns\";\n\nexport interface Page {\n path: string\n key: string\n params: string[]\n regex: RegExp\n}\n\nexport interface Layout {\n dir: string\n key: string\n}\n\nexport interface PagesResult {\n pages: Page[]\n layouts: Layout[]\n}\n\nfunction keyToRoutePattern(key: string, pagesDir: string): string {\n const rel = key.slice(pagesDir.length + 1).replace(/\\\\/g, '/')\n const pattern = routePattern(rel)\n return pattern === \"/\" ? \"/\" : `/${pattern}`\n}\n\nfunction keyToDir(key: string): string {\n return key.slice(0, key.lastIndexOf('/'))\n}\n\nlet cache: PagesResult | null = null\n\nexport function invalidatePagesCache() {\n cache = null\n}\n\nexport function buildPages(pageKeys: string[], layoutKeys: string[], pagesDir: string): PagesResult {\n if (cache) return cache\n\n const pages: Page[] = []\n const layouts: Layout[] = []\n\n for (const key of layoutKeys) {\n layouts.push({dir: keyToDir(key), key})\n }\n\n for (const key of pageKeys) {\n const pattern = keyToRoutePattern(key, pagesDir)\n const params = [...pattern.matchAll(/:([^/]+)/g)].map(m => m[1])\n const regexStr = pattern\n .replace(/:[^/]+/g, '([^/]+)')\n .replace(/\\//g, '\\\\/')\n pages.push({path: pattern, key, params, regex: new RegExp(`^${regexStr}$`)})\n }\n\n pages.sort((a, b) => {\n const aScore = (a.path.match(/:/g) || []).length\n const bScore = (b.path.match(/:/g) || []).length\n if (aScore !== bScore) return aScore - bScore\n return b.path.length - a.path.length\n })\n\n cache = {pages, layouts}\n return cache\n}\n\nexport function collectLayoutChain(pageKey: string, layouts: Layout[]): Layout[] {\n const pageDir = keyToDir(pageKey)\n\n return layouts\n .filter(layout => pageDir.startsWith(layout.dir))\n .sort((a, b) => a.dir.split('/').length - b.dir.split('/').length)\n}\n\nexport function matchPage(pathname: string, pages: Page[]): {\n page: Page\n params: Record<string, string>\n} | null {\n for (const page of pages) {\n const match = pathname.match(page.regex)\n if (match) {\n const params: Record<string, string> = {}\n page.params.forEach((name, i) => {\n params[name] = decodeURIComponent(match[i + 1])\n })\n return {page, params}\n }\n }\n return null\n}\n", "import {routePattern} from \"../utils/patterns\";\n\nexport interface ApiRoute {\n path: string\n key: string\n params: string[]\n regex: RegExp\n}\n\nexport interface ApiMiddleware {\n dir: string\n key: string\n}\n\nexport interface ApiResult {\n routes: ApiRoute[]\n middlewares: ApiMiddleware[]\n}\n\nexport function keyToRoutePattern(key: string, apiDir: string): string {\n const rel = key.slice(apiDir.length + 1).replace(/\\\\/g, '/')\n const pattern = routePattern(rel)\n return pattern === '/' ? '/api' : `/api/${pattern}`.replace('/api//', '/api/')\n}\n\nfunction keyToDir(key: string): string {\n return key.slice(0, key.lastIndexOf('/'))\n}\n\nlet cache: ApiResult | null = null\n\nexport function invalidateApiCache() {\n cache = null\n}\n\nexport function buildRoutes(routeKeys: string[], middlewareKeys: string[], apiDir: string): ApiResult {\n if (cache) return cache\n\n const routes: ApiRoute[] = []\n const middlewares: ApiMiddleware[] = []\n\n for (const key of middlewareKeys) {\n middlewares.push({dir: keyToDir(key), key})\n }\n\n for (const key of routeKeys) {\n const pattern = keyToRoutePattern(key, apiDir)\n const params = [...pattern.matchAll(/:([^/]+)/g)].map(m => m[1])\n const regexStr = pattern\n .replace(/:[^/]+/g, '([^/]+)')\n .replace(/\\//g, '\\\\/')\n routes.push({path: pattern, key, params, regex: new RegExp(`^${regexStr}$`)})\n }\n routes.sort((a, b) => {\n const aScore = (a.path.match(/:/g) || []).length\n const bScore = (b.path.match(/:/g) || []).length\n if (aScore !== bScore) return aScore - bScore\n return b.path.length - a.path.length\n })\n\n cache = {routes, middlewares}\n return cache\n}\n\nexport function collectMiddlewareChain(routeKey: string, middlewares: ApiMiddleware[]): ApiMiddleware[] {\n const routeDir = keyToDir(routeKey)\n\n return middlewares\n .filter(mw => routeDir.startsWith(mw.dir))\n .sort((a, b) => a.dir.split('/').length - b.dir.split('/').length)\n}\n\nexport function matchRoute(\n pathname: string,\n routes: ApiRoute[]\n): {route: ApiRoute; params: Record<string, string>} | null {\n for (const route of routes) {\n const match = pathname.match(route.regex)\n if (match) {\n const params: Record<string, string> = {}\n route.params.forEach((name, i) => {\n params[name] = decodeURIComponent(match[i + 1])\n })\n return {route, params}\n }\n }\n return null\n}\n", "export function generateContext(): string {\n return `\nexport {RouterContext} from '@devlusoft/devix/runtime/context'\n`\n}", "import {readFileSync, readdirSync, statSync} from 'node:fs'\nimport {join, relative} from 'node:path'\nimport {extractHttpMethods} from './extract-methods'\nimport {buildRouteEntry} from './routes-dts'\nimport type {RouteEntry} from './routes-dts'\n\nfunction walkDir(dir: string, root: string): string[] {\n const entries: string[] = []\n for (const name of readdirSync(dir)) {\n const full = join(dir, name)\n if (statSync(full).isDirectory()) {\n entries.push(...walkDir(full, root))\n } else if (/\\.(ts|tsx)$/.test(name)) {\n entries.push(relative(root, full).replace(/\\\\/g, '/'))\n }\n }\n return entries\n}\n\nexport function scanApiFiles(appDir: string, projectRoot: string): RouteEntry[] {\n const apiDir = join(projectRoot, appDir, 'api')\n\n let files: string[]\n try {\n files = walkDir(apiDir, projectRoot)\n } catch {\n return []\n }\n\n return files\n .filter(f => !f.endsWith('middleware.ts') && !f.endsWith('middleware.tsx'))\n .flatMap(filePath => {\n try {\n const content = readFileSync(join(projectRoot, filePath), 'utf-8')\n const methods = extractHttpMethods(content)\n if (methods.length === 0) return []\n return [buildRouteEntry(filePath, `${appDir}/api`, methods)]\n } catch {\n return []\n }\n })\n}\n", "const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const\nexport type HttpMethod = (typeof HTTP_METHODS)[number]\n\nconst METHOD_EXPORT_RE = /export\\s+(?:const|async\\s+function|function)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b/g\n\nfunction stripComments(content: string): string {\n return content\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/\\/\\/.*$/gm, '')\n}\n\nexport function extractHttpMethods(content: string): HttpMethod[] {\n const found = new Set<HttpMethod>()\n for (const match of stripComments(content).matchAll(METHOD_EXPORT_RE)) {\n found.add(match[1] as HttpMethod)\n }\n return [...found]\n}\n", "import { keyToRoutePattern } from '../../server/api-router'\nimport type { HttpMethod } from './extract-methods'\n\nexport interface RouteEntry {\n filePath: string\n urlPattern: string\n identifier: string\n methods: HttpMethod[]\n}\n\nexport function filePathToIdentifier(filePath: string, apiDir: string): string {\n return '_api_' + filePath\n .slice(`${apiDir}/`.length)\n .replace(/\\.(ts|tsx)$/, '')\n .replace(/[^a-zA-Z0-9]/g, '_')\n}\n\nexport function buildRouteEntry(filePath: string, apiDir: string, methods: HttpMethod[]): RouteEntry {\n return {\n filePath,\n urlPattern: keyToRoutePattern(filePath, apiDir),\n identifier: filePathToIdentifier(filePath, apiDir),\n methods,\n }\n}\n\nexport function generateRoutesDts(entries: RouteEntry[], apiDir: string): string {\n if (entries.length === 0) {\n return `// auto-generado por devix \u2014 no editar\\ndeclare module '@devlusoft/devix' {\\n interface ApiRoutes {}\\n}\\n`\n }\n\n const imports = entries\n .map(e => {\n const importPath = '../' + e.filePath.replace(/\\.(ts|tsx)$/, '')\n return `import type * as ${e.identifier} from '${importPath}'`\n })\n .join('\\n')\n\n const routeLines = entries.flatMap(e =>\n e.methods.map(m =>\n ` '${m} ${e.urlPattern}': InferRoute<(typeof ${e.identifier})['${m}']>`\n )\n ).join('\\n')\n\n return `// auto-generado por devix \u2014 no editar\n${imports}\n\ntype InferRoute<T> = T extends (...args: any[]) => any\n ? Exclude<Awaited<ReturnType<T>>, Response | null | void>\n : never\n\ndeclare module '@devlusoft/devix' {\n interface ApiRoutes {\n${routeLines}\n }\n}\n`\n}\n", "import {mkdirSync, readFileSync, writeFileSync, existsSync} from 'node:fs'\nimport {join} from 'node:path'\n\nexport function writeRoutesDts(content: string, projectRoot: string): boolean {\n const devixDir = join(projectRoot, '.devix')\n const outPath = join(devixDir, 'routes.d.ts')\n\n mkdirSync(devixDir, {recursive: true})\n\n if (existsSync(outPath) && readFileSync(outPath, 'utf-8') === content) {\n return false\n }\n\n writeFileSync(outPath, content, 'utf-8')\n return true\n}\n", "export function parseDuration(value: number | string): number {\n if (typeof value === 'number') return value\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h)?$/)\n if (!match) throw new Error(`[devix] Invalid duration: \"${value}\". Use a number (ms) or a string like \"5s\", \"2m\", \"500ms\".`)\n const n = parseFloat(match[1])\n switch (match[2]) {\n case 'h': return n * 3_600_000\n case 'm': return n * 60_000\n case 's': return n * 1_000\n case 'ms':\n default: return n\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["writeFileSync", "resolve", "cache", "readFileSync", "join", "config", "writeFileSync", "resolve"]
|
|
7
7
|
}
|
package/dist/cli/dev.js
CHANGED
|
@@ -152,6 +152,11 @@ export function handleApiRequest(url, request) {
|
|
|
152
152
|
`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// src/utils/patterns.ts
|
|
156
|
+
function routePattern(rel) {
|
|
157
|
+
return rel.replace(/\.(tsx|ts|jsx|js)$/, "").replace(/\(.*?\)\//g, "").replace(/^index$|\/index$/, "").replace(/\[([^\]]+)]/g, ":$1") || "/";
|
|
158
|
+
}
|
|
159
|
+
|
|
155
160
|
// src/server/pages-router.ts
|
|
156
161
|
var cache = null;
|
|
157
162
|
function invalidatePagesCache() {
|
|
@@ -159,6 +164,11 @@ function invalidatePagesCache() {
|
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
// src/server/api-router.ts
|
|
167
|
+
function keyToRoutePattern(key, apiDir) {
|
|
168
|
+
const rel = key.slice(apiDir.length + 1).replace(/\\/g, "/");
|
|
169
|
+
const pattern = routePattern(rel);
|
|
170
|
+
return pattern === "/" ? "/api" : `/api/${pattern}`.replace("/api//", "/api/");
|
|
171
|
+
}
|
|
162
172
|
var cache2 = null;
|
|
163
173
|
function invalidateApiCache() {
|
|
164
174
|
cache2 = null;
|
|
@@ -171,6 +181,114 @@ export {RouterContext} from '@devlusoft/devix/runtime/context'
|
|
|
171
181
|
`;
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
// src/vite/codegen/scan-api.ts
|
|
185
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
186
|
+
import { join, relative } from "node:path";
|
|
187
|
+
|
|
188
|
+
// src/vite/codegen/extract-methods.ts
|
|
189
|
+
var METHOD_EXPORT_RE = /export\s+(?:const|async\s+function|function)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
|
|
190
|
+
function stripComments(content) {
|
|
191
|
+
return content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
192
|
+
}
|
|
193
|
+
function extractHttpMethods(content) {
|
|
194
|
+
const found = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const match of stripComments(content).matchAll(METHOD_EXPORT_RE)) {
|
|
196
|
+
found.add(match[1]);
|
|
197
|
+
}
|
|
198
|
+
return [...found];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/vite/codegen/routes-dts.ts
|
|
202
|
+
function filePathToIdentifier(filePath, apiDir) {
|
|
203
|
+
return "_api_" + filePath.slice(`${apiDir}/`.length).replace(/\.(ts|tsx)$/, "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
204
|
+
}
|
|
205
|
+
function buildRouteEntry(filePath, apiDir, methods) {
|
|
206
|
+
return {
|
|
207
|
+
filePath,
|
|
208
|
+
urlPattern: keyToRoutePattern(filePath, apiDir),
|
|
209
|
+
identifier: filePathToIdentifier(filePath, apiDir),
|
|
210
|
+
methods
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function generateRoutesDts(entries, apiDir) {
|
|
214
|
+
if (entries.length === 0) {
|
|
215
|
+
return `// auto-generado por devix \u2014 no editar
|
|
216
|
+
declare module '@devlusoft/devix' {
|
|
217
|
+
interface ApiRoutes {}
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
const imports = entries.map((e) => {
|
|
222
|
+
const importPath = "../" + e.filePath.replace(/\.(ts|tsx)$/, "");
|
|
223
|
+
return `import type * as ${e.identifier} from '${importPath}'`;
|
|
224
|
+
}).join("\n");
|
|
225
|
+
const routeLines = entries.flatMap(
|
|
226
|
+
(e) => e.methods.map(
|
|
227
|
+
(m) => ` '${m} ${e.urlPattern}': InferRoute<(typeof ${e.identifier})['${m}']>`
|
|
228
|
+
)
|
|
229
|
+
).join("\n");
|
|
230
|
+
return `// auto-generado por devix \u2014 no editar
|
|
231
|
+
${imports}
|
|
232
|
+
|
|
233
|
+
type InferRoute<T> = T extends (...args: any[]) => any
|
|
234
|
+
? Exclude<Awaited<ReturnType<T>>, Response | null | void>
|
|
235
|
+
: never
|
|
236
|
+
|
|
237
|
+
declare module '@devlusoft/devix' {
|
|
238
|
+
interface ApiRoutes {
|
|
239
|
+
${routeLines}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/vite/codegen/scan-api.ts
|
|
246
|
+
function walkDir(dir, root) {
|
|
247
|
+
const entries = [];
|
|
248
|
+
for (const name of readdirSync(dir)) {
|
|
249
|
+
const full = join(dir, name);
|
|
250
|
+
if (statSync(full).isDirectory()) {
|
|
251
|
+
entries.push(...walkDir(full, root));
|
|
252
|
+
} else if (/\.(ts|tsx)$/.test(name)) {
|
|
253
|
+
entries.push(relative(root, full).replace(/\\/g, "/"));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return entries;
|
|
257
|
+
}
|
|
258
|
+
function scanApiFiles(appDir, projectRoot) {
|
|
259
|
+
const apiDir = join(projectRoot, appDir, "api");
|
|
260
|
+
let files;
|
|
261
|
+
try {
|
|
262
|
+
files = walkDir(apiDir, projectRoot);
|
|
263
|
+
} catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
return files.filter((f) => !f.endsWith("middleware.ts") && !f.endsWith("middleware.tsx")).flatMap((filePath) => {
|
|
267
|
+
try {
|
|
268
|
+
const content = readFileSync(join(projectRoot, filePath), "utf-8");
|
|
269
|
+
const methods = extractHttpMethods(content);
|
|
270
|
+
if (methods.length === 0) return [];
|
|
271
|
+
return [buildRouteEntry(filePath, `${appDir}/api`, methods)];
|
|
272
|
+
} catch {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/vite/codegen/write-routes-dts.ts
|
|
279
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync } from "node:fs";
|
|
280
|
+
import { join as join2 } from "node:path";
|
|
281
|
+
function writeRoutesDts(content, projectRoot) {
|
|
282
|
+
const devixDir = join2(projectRoot, ".devix");
|
|
283
|
+
const outPath = join2(devixDir, "routes.d.ts");
|
|
284
|
+
mkdirSync(devixDir, { recursive: true });
|
|
285
|
+
if (existsSync(outPath) && readFileSync2(outPath, "utf-8") === content) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
writeFileSync(outPath, content, "utf-8");
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
174
292
|
// src/vite/index.ts
|
|
175
293
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
176
294
|
var VIRTUAL_ENTRY_CLIENT = "virtual:devix/entry-client";
|
|
@@ -207,14 +325,35 @@ function devix(config2) {
|
|
|
207
325
|
if (id === `\0${VIRTUAL_CONTEXT}`)
|
|
208
326
|
return generateContext();
|
|
209
327
|
},
|
|
328
|
+
buildStart() {
|
|
329
|
+
const root = process.cwd();
|
|
330
|
+
const entries = scanApiFiles(appDir, root);
|
|
331
|
+
writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root);
|
|
332
|
+
},
|
|
210
333
|
configureServer(server) {
|
|
334
|
+
const root = process.cwd();
|
|
335
|
+
const regenerateDts = () => {
|
|
336
|
+
const entries = scanApiFiles(appDir, root);
|
|
337
|
+
writeRoutesDts(generateRoutesDts(entries, `${appDir}/api`), root);
|
|
338
|
+
};
|
|
211
339
|
server.watcher.on("add", (file) => {
|
|
212
|
-
if (file.startsWith(resolve(
|
|
213
|
-
if (file.includes(`${appDir}/api`))
|
|
340
|
+
if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache();
|
|
341
|
+
if (file.includes(`${appDir}/api`)) {
|
|
342
|
+
invalidateApiCache();
|
|
343
|
+
regenerateDts();
|
|
344
|
+
}
|
|
214
345
|
});
|
|
215
346
|
server.watcher.on("unlink", (file) => {
|
|
216
|
-
if (file.startsWith(resolve(
|
|
217
|
-
if (file.includes(`${appDir}/api`))
|
|
347
|
+
if (file.startsWith(resolve(root, pagesDir))) invalidatePagesCache();
|
|
348
|
+
if (file.includes(`${appDir}/api`)) {
|
|
349
|
+
invalidateApiCache();
|
|
350
|
+
regenerateDts();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
server.watcher.on("change", (file) => {
|
|
354
|
+
if (file.includes(`${appDir}/api`) && !file.endsWith("middleware.ts")) {
|
|
355
|
+
regenerateDts();
|
|
356
|
+
}
|
|
218
357
|
});
|
|
219
358
|
}
|
|
220
359
|
};
|
|
@@ -311,7 +450,19 @@ function parseDuration(value) {
|
|
|
311
450
|
}
|
|
312
451
|
}
|
|
313
452
|
|
|
453
|
+
// src/utils/env.ts
|
|
454
|
+
import { loadEnv } from "vite";
|
|
455
|
+
function loadDotenv(mode) {
|
|
456
|
+
const env = loadEnv(mode, process.cwd(), "");
|
|
457
|
+
for (const [key, value] of Object.entries(env)) {
|
|
458
|
+
if (process.env[key] === void 0) {
|
|
459
|
+
process.env[key] = value;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
314
464
|
// src/cli/dev.ts
|
|
465
|
+
loadDotenv("development");
|
|
315
466
|
var VIRTUAL_RENDER2 = "virtual:devix/render";
|
|
316
467
|
var VIRTUAL_API2 = "virtual:devix/api";
|
|
317
468
|
var config = (await import(`${process.cwd()}/devix.config.ts`)).default;
|