feat: move cli to core (#55)

* refactor: move cli to core

* feat: move cli to core

- use route-middleware in serve
- eliminate need for `routes` dir in app

* feat: use route-middleware in build

* chore: update test gh action
This commit is contained in:
Ayo Ayco 2025-01-08 21:21:31 +01:00 committed by GitHub
parent f08ce4c043
commit 73617647db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 227 additions and 184 deletions

View file

@ -21,8 +21,8 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Install CLI deps
run: cd packages/cli && npm install --ignore-scripts
- name: Install core deps
run: cd packages/core && npm install --ignore-scripts
- name: Run CLI tests
run: npx vitest run .

View file

@ -5,6 +5,7 @@
"consola",
"estree",
"giget",
"hookable",
"mcfly",
"mcflyjs",
"nitropack",

View file

@ -8,7 +8,7 @@
"build:preview": "pnpm --filter @mcflyjs/landing-page build:preview",
"template:basic": "pnpm --filter @templates/basic dev",
"create": "node ./packages/create-mcfly",
"cli": "node ./packages/cli",
"cli": "node ./packages/core/cli/index.js",
"test": "vitest .",
"lint": "eslint . --config eslint.config.mjs --cache",
"check": "npm run format && npm run lint",

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Ayo Ayco
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,27 +0,0 @@
# McFly CLI
The **McFly CLI** is the command-line tooling for [McFly](https://ayco.io/gh/McFly) -- a no-framework framework that assists in leveraging the web platform.
## Installation
Install the CLI using npm:
```
npm i -g @mcflyjs/cli
```
## Commands
You can run the following **McFly CLI** commands:
| Command | Action |
| --- | --- |
| `mcfly new` | Creates a new McFly project. |
| `mcfly serve` | Runs the developent server. |
| `mcfly build` | Builds the McFly project for production. |
| `mcfly prepare` | Prepares the McFly workspace. |
| `mcfly generate` | Generates building blocks for a McFly app. (In-progress) |
---
*Just keep building*<br />
*A project by [Ayo Ayco](https://ayco.io)*

View file

@ -1,41 +0,0 @@
{
"name": "@mcflyjs/cli",
"version": "0.1.1",
"description": "McFly CLI tools",
"type": "module",
"main": "index.js",
"bin": {
"mcfly": "./index.js"
},
"scripts": {
"version": "npm version",
"publish": "npm publish",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ayoayco/McFly.git",
"directory": "packages/cli"
},
"author": "Ayo Ayco",
"license": "MIT",
"bugs": {
"url": "https://github.com/ayoayco/McFly/issues"
},
"homepage": "https://mcfly.js.org",
"dependencies": {
"c12": "^2.0.1",
"citty": "^0.1.6",
"consola": "^3.3.3",
"pathe": "^1.1.2"
},
"devDependencies": {
"@vitest/coverage-istanbul": "2.1.8",
"@vitest/coverage-v8": "2.1.8",
"@vitest/ui": "2.1.8",
"nitropack": "~2.10.4",
"vitest": "^2.1.8"
}
}

View file

@ -1,12 +0,0 @@
import { test } from 'vitest'
import { exportedForTest } from '..'
import { expect } from 'vitest'
const testObj = exportedForTest.main
test('should have correct subcommands', () => {
Object.keys(testObj.subCommands).forEach((key) => {
expect(testObj.subCommands[key]).toBeTypeOf('function')
expect(testObj.subCommands[key].name).toBe(key)
})
})

View file

@ -2,7 +2,7 @@
import { consola } from 'consola'
import { defineCommand } from 'citty'
import { resolve } from 'pathe'
import { dirname, resolve } from 'pathe'
import { loadConfig } from 'c12'
import {
build,
@ -11,6 +11,7 @@ import {
prepare,
prerender,
} from 'nitropack'
import { fileURLToPath } from 'node:url'
async function _build(args) {
consola.start('Building project...')
@ -30,6 +31,15 @@ async function _build(args) {
...(nitroConfig ?? {}),
})
nitro.options.runtimeConfig.mcfly = mcflyConfig
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
nitro.options.handlers.push({
middleware: true,
handler: resolve(__dirname, '../../route-middleware.js'),
})
await prepare(nitro)
await copyPublicAssets(nitro)
await prerender(nitro)
@ -42,7 +52,7 @@ async function _build(args) {
export default defineCommand({
meta: {
name: 'prepare',
name: 'build',
description: 'Builds the McFly project for production.',
},

View file

@ -9,7 +9,7 @@ function generate() {
export default defineCommand({
meta: {
name: 'prepare',
name: 'generate',
description: 'Generates building blocks for a McFly app.',
},
run() {

View file

@ -18,7 +18,7 @@ function createNew(args) {
export default defineCommand({
meta: {
name: 'prepare',
name: 'new',
description: 'Creates a new McFly project.',
},
args: {

View file

@ -13,8 +13,12 @@ import {
} from 'nitropack'
import { resolve } from 'pathe'
import { loadConfig } from 'c12'
import { fileURLToPath } from 'node:url'
import { dirname } from 'pathe'
const hmrKeyRe = /^runtimeConfig\.|routeRules\./
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
async function printInfo() {
try {
@ -91,7 +95,13 @@ async function serve(args) {
)
nitro.hooks.hookOnce('restart', reload)
nitro.options.runtimeConfig.mcfly = mcflyConfig
nitro.options.handlers.push({
middleware: true,
handler: resolve(__dirname, '../../route-middleware.js'),
})
const server = createDevServer(nitro)
// const listenOptions = parseArgs(args)
await server.listen(1234)
await prepare(nitro)
@ -106,7 +116,7 @@ async function serve(args) {
export default defineCommand({
meta: {
name: 'prepare',
name: 'serve',
description: 'Runs the dev server.',
},
async run({ args }) {

View file

@ -1,11 +1,11 @@
import { consola } from 'consola'
import { eventHandler } from 'h3'
import { createHooks } from 'hookable'
import { hooks as mcflyHooks } from './hooks.js'
import { evaluateServerScripts } from './evaluate-scripts.js'
import { injectHtmlFragments } from './inject-fragments.js'
import { injectCustomElements } from './inject-elements.js'
import defaultMcflyConfig from './default-mcfly-config.js'
import { hooks as mcflyHooks } from './hooks.mjs'
import { evaluateServerScripts } from './evaluate-scripts.mjs'
import { injectHtmlFragments } from './inject-fragments.mjs'
import { injectCustomElements } from './inject-elements.mjs'
import defaultMcflyConfig from './default-mcfly-config.mjs'
import { loadConfig } from 'c12'
/**
@ -17,16 +17,16 @@ import { loadConfig } from 'c12'
/**
* Intercepts all routes and assembles the correct HTML to return
* @deprecated
* @param {{
* storage: Storage
* }} param0
* @returns {EventHandler}
*/
export function useMcFlyRoute({ storage }) {
return eventHandler(async (event) => {
const hooks = createHooks()
Object.keys(mcflyHooks).forEach((hookName) => hooks.addHooks(hookName))
return eventHandler(async (event) => {
const { path } = event
let { config } = await loadConfig({ name: 'mcfly' })

View file

@ -1,8 +1,8 @@
import { ELEMENT_NODE, parse, render, walkSync } from 'ultrahtml'
import { getFiles } from './get-files.js'
import { getFiles } from './get-files.mjs'
/**
* @typedef {import('../config').McFlyConfig} Config
* @typedef {import('../config/index.js').McFlyConfig} Config
*/
/**

View file

@ -1,5 +1,5 @@
import { ELEMENT_NODE, parse, render, walkSync } from 'ultrahtml'
import { getFiles } from './get-files.js'
import { getFiles } from './get-files.mjs'
/**
* @typedef {import('ultrahtml').Node} HtmlNode

View file

@ -1,13 +1,17 @@
{
"name": "@mcflyjs/core",
"version": "0.6.2",
"version": "0.7.0",
"description": "McFly core package",
"type": "module",
"main": "index.js",
"bin": {
"mcfly": "./cli/index.js"
},
"scripts": {
"version": "npm version",
"publish": "npm publish",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build:middleware": "npx esbuild route-middleware.js --bundle --outfile=mcfly-middleware.js"
},
"repository": {
"type": "git",
@ -22,8 +26,8 @@
"homepage": "https://mcfly.js.org",
"dependencies": {
"c12": "^2.0.1",
"citty": "^0.1.6",
"consola": "^3.3.3",
"esbuild": "^0.24.2",
"esprima": "^4.0.1",
"h3": "^1.13.0",
"nitropack": "latest",
@ -31,7 +35,12 @@
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"@vitest/coverage-istanbul": "2.1.8",
"@vitest/coverage-v8": "2.1.8",
"@vitest/ui": "2.1.8",
"esbuild": "^0.24.2",
"hookable": "^5.5.3",
"unstorage": "^1.14.4"
"unstorage": "^1.14.4",
"vitest": "^2.1.8"
}
}

View file

@ -0,0 +1,124 @@
import { eventHandler } from 'h3'
import { useRuntimeConfig, useStorage } from 'nitropack/runtime'
import { createHooks } from 'hookable'
import { consola } from 'consola'
import { hooks as mcflyHooks } from '@mcflyjs/core/hooks.mjs'
import defaultMcflyConfig from '@mcflyjs/core/default-mcfly-config.mjs'
import { evaluateServerScripts } from '@mcflyjs/core/evaluate-scripts.mjs'
import { injectHtmlFragments } from '@mcflyjs/core/inject-fragments.mjs'
import { injectCustomElements } from '@mcflyjs/core/inject-elements.mjs'
/**
* @typedef {import('../config').McFlyConfig} Config
* @typedef {import('unstorage').Storage} Storage
* @typedef {import('unstorage').StorageValue} StorageValue
* @typedef {import('h3').EventHandler} EventHandler
*/
/**
* McFly middleware event handler
*/
export default eventHandler(async (event) => {
const hooks = createHooks()
Object.keys(mcflyHooks).forEach((hookName) => hooks.addHooks(hookName))
const { path } = event
let { mcfly: config } = useRuntimeConfig()
const storage = useStorage()
if (!config || Object.keys(config).length === 0) {
config = defaultMcflyConfig
consola.warn(`[WARN]: McFly configuration not loaded, using defaults...`)
}
const plugins = config.plugins
plugins.forEach((plugin) => {
const pluginHooks = Object.keys(plugin)
pluginHooks.forEach((pluginHook) => {
hooks.hook(pluginHook, plugin[pluginHook])
})
})
const { components: componentType } = config
let html = await getHtml(path, storage)
if (html) {
const transforms = [
{
fn: evaluateServerScripts,
args: [''],
hook: mcflyHooks.serverScriptsEvaluated,
},
{
fn: injectHtmlFragments,
args: [storage],
hook: mcflyHooks.fragmentsInjected,
},
{
fn: injectCustomElements,
args: [componentType, storage],
hook: mcflyHooks.customElementsInjected,
},
]
if (!!componentType && !!html) {
for (const transform of transforms) {
html = await transform.fn(html.toString(), ...transform.args)
// call hook
if (transform.hook) {
hooks.callHook(transform.hook)
}
}
} else {
consola.error('[ERR]: Failed to insert registry', {
componentType: !componentType ? 'missing' : 'okay',
html: !html ? 'missing' : 'okay',
})
}
}
if (html) {
hooks.callHook(mcflyHooks.pageRendered)
}
return (
html ??
new Response(
'😱 ERROR 404: Not found. You can put a 404.html on the ./src/pages directory to customize this error page.',
{ status: 404 }
)
)
})
/**
* Gets the storage path for a file
* @param {string} filename
* @returns {string}
*/
function getPath(filename) {
return `assets:pages${filename}`
}
function getPurePath(path) {
return path.split('?')[0]
}
/**
* Gets the correct HTML depending on the path requested
* @param {string} path
* @param {Storage} storage
* @returns {Promise<StorageValue>}
*/
async function getHtml(path, storage) {
const purePath = getPurePath(path)
const rawPath =
purePath[purePath.length - 1] === '/' ? purePath.slice(0, -1) : purePath
const filename = rawPath === '' ? '/index.html' : `${rawPath}.html`
const fallback = getPath(rawPath + '/index.html')
const filePath = getPath(filename)
let html = await storage.getItem(filePath)
if (!html) html = await storage.getItem(fallback)
if (!html) html = await storage.getItem(getPath('/404.html'))
return html
}

View file

@ -1,6 +1,6 @@
import consola from 'consola'
import { vi, expect, test } from 'vitest'
import { exportedForTest } from '../commands/build.mjs'
import { exportedForTest } from '../cli/commands/build.mjs'
const testFn = exportedForTest.build

View file

@ -1,5 +1,5 @@
import { expect, test, vi } from 'vitest'
import { exportedForTest } from '../commands/generate.mjs'
import { exportedForTest } from '../cli/commands/generate.mjs'
import consola from 'consola'
const testFn = exportedForTest.generate

View file

@ -1,5 +1,5 @@
import { expect, test, vi } from 'vitest'
import { exportedForTest } from '../commands/new.mjs'
import { exportedForTest } from '../cli/commands/new.mjs'
import { execSync } from 'node:child_process'
import consola from 'consola'

View file

@ -1,5 +1,5 @@
import { test, expect, vi } from 'vitest'
import { exportedForTest } from '../commands/prepare.mjs'
import { exportedForTest } from '../cli/commands/prepare.mjs'
import consola from 'consola'
import { execSync } from 'node:child_process'

View file

@ -1,5 +1,5 @@
import { describe, expect, test, vi } from 'vitest'
import { exportedForTest } from '../commands/serve.mjs'
import { exportedForTest } from '../cli/commands/serve.mjs'
import consola from 'consola'
// describe.skip('FUNCTION: serve()', () => {

View file

@ -36,37 +36,6 @@ importers:
specifier: ^2.1.8
version: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(terser@5.37.0)
packages/cli:
dependencies:
c12:
specifier: ^2.0.1
version: 2.0.1(magicast@0.3.5)
citty:
specifier: ^0.1.6
version: 0.1.6
consola:
specifier: ^3.3.3
version: 3.3.3
pathe:
specifier: ^1.1.2
version: 1.1.2
devDependencies:
'@vitest/coverage-istanbul':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
'@vitest/coverage-v8':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
'@vitest/ui':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
nitropack:
specifier: ~2.10.4
version: 2.10.4(typescript@5.7.2)
vitest:
specifier: ^2.1.8
version: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(terser@5.37.0)
packages/config:
dependencies:
h3:
@ -85,12 +54,12 @@ importers:
c12:
specifier: ^2.0.1
version: 2.0.1(magicast@0.3.5)
citty:
specifier: ^0.1.6
version: 0.1.6
consola:
specifier: ^3.3.3
version: 3.3.3
esbuild:
specifier: ^0.24.2
version: 0.24.2
esprima:
specifier: ^4.0.1
version: 4.0.1
@ -107,12 +76,27 @@ importers:
specifier: ^1.5.3
version: 1.5.3
devDependencies:
'@vitest/coverage-istanbul':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
'@vitest/coverage-v8':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
'@vitest/ui':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8)
esbuild:
specifier: ^0.24.2
version: 0.24.2
hookable:
specifier: ^5.5.3
version: 5.5.3
unstorage:
specifier: ^1.14.4
version: 1.14.4(db0@0.2.1)(ioredis@5.4.2)
vitest:
specifier: ^2.1.8
version: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(terser@5.37.0)
packages/create-mcfly:
dependencies:
@ -125,9 +109,6 @@ importers:
site:
dependencies:
'@mcflyjs/cli':
specifier: workspace:*
version: link:../packages/cli
'@mcflyjs/config':
specifier: workspace:*
version: link:../packages/config
@ -139,7 +120,7 @@ importers:
dependencies:
'@mcflyjs/cli':
specifier: ^0.1.1
version: link:../../packages/cli
version: 0.1.1(magicast@0.3.5)
'@mcflyjs/config':
specifier: ^0.2.1
version: link:../../packages/config
@ -622,6 +603,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@mcflyjs/cli@0.1.1':
resolution: {integrity: sha512-YT0XVwbUO9SscfoCrNY1Tz92SwLrCKbvHYrPUWjC8UAVaaX8tekdfRDF7DiIr8PX5me3YIZUBnQw0/loFNPhvQ==}
hasBin: true
'@mcflyjs/core@0.6.1':
resolution: {integrity: sha512-ozgpIRpNix7gyuFlB8ofEm+iENMvxQsgyzxMTbIrOM69QYiEV1GsjxWyUqH3QuCwA4UB1KHdyWS+PNaCWSepOQ==}
@ -3132,6 +3117,15 @@ snapshots:
- encoding
- supports-color
'@mcflyjs/cli@0.1.1(magicast@0.3.5)':
dependencies:
c12: 2.0.1(magicast@0.3.5)
citty: 0.1.6
consola: 3.3.3
pathe: 1.1.2
transitivePeerDependencies:
- magicast
'@mcflyjs/core@0.6.1(magicast@0.3.5)(typescript@5.7.2)':
dependencies:
c12: 2.0.1(magicast@0.3.5)

View file

@ -2,19 +2,6 @@
"name": "@mcflyjs/landing-page",
"description": "McFly is a full-stack no-framework framework that assists developers in leveraging the web platform.",
"private": true,
"scripts": {
"start": "mcfly serve",
"prepare": "mcfly prepare",
"dev": "mcfly serve",
"build": "mcfly build",
"preview": "node .output/server/index.mjs",
"build:preview": "pnpm run build && pnpm run preview"
},
"dependencies": {
"@mcflyjs/cli": "workspace:*",
"@mcflyjs/config": "workspace:*",
"@mcflyjs/core": "workspace:*"
},
"version": "0.0.1",
"main": "index.js",
"repository": {
@ -27,5 +14,17 @@
"bugs": {
"url": "https://github.com/ayoayco/McFly/issues"
},
"homepage": "https://mcfly.js.org"
"homepage": "https://mcfly.js.org",
"scripts": {
"start": "mcfly serve",
"prepare": "mcfly prepare",
"dev": "mcfly serve",
"build": "mcfly build",
"preview": "node .output/server/index.mjs",
"build:preview": "pnpm run build && pnpm run preview"
},
"dependencies": {
"@mcflyjs/config": "workspace:*",
"@mcflyjs/core": "workspace:*"
}
}

View file

@ -1,3 +0,0 @@
import { useMcFlyRoute } from '@mcflyjs/core'
export default useMcFlyRoute({ storage: useStorage() })