diff --git a/bun.lockb b/bun.lockb index 987a9a8a..7ce0d3d2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 9b7f6dce..b9648a85 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "type": "module", "scripts": { "build": "rimraf src/_lib src/tsconfig.build.* && tsc -p tsconfig.build.json && bun scripts/postbuild.ts", - "dev": "cd playgrounds/default && bun run dev", + "docs:dev": "cd site && bun run dev", + "docs:build": "cd site && bun run build", "format": "biome format . --write", "lint": "biome lint . --apply-unsafe", + "playground": "cd playgrounds/default && bun run dev", "prepare": "bun x simple-git-hooks", "typecheck": "tsc --noEmit" }, diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts index 04cb76a9..0ddd44c7 100644 --- a/scripts/postbuild.ts +++ b/scripts/postbuild.ts @@ -4,15 +4,24 @@ import { readdirSync } from 'node:fs' import { resolve } from 'node:path' import { copy, copyFileSync, readFileSync, writeFileSync } from 'fs-extra' +// Copy index.html copyFileSync( resolve(import.meta.dir, '../src/vite/index.html'), resolve(import.meta.dir, '../src/_lib/vite/index.html'), ) + +// Copy styles copy( resolve(import.meta.dir, '../src/app/styles'), resolve(import.meta.dir, '../src/_lib/app/styles'), ) +// Copy CLI init templates +copy( + resolve(import.meta.dir, '../src/cli/templates'), + resolve(import.meta.dir, '../src/_lib/cli/templates'), +) + rewriteExtensions(resolve(import.meta.dir, '../src/_lib')) //////////////////////////////////////////////////////////////////// diff --git a/site/package.json b/site/package.json index c7f9c292..c3380d88 100644 --- a/site/package.json +++ b/site/package.json @@ -3,9 +3,9 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "bun ../src/cli.ts", - "build": "NODE_ENV=production bun ../src/cli.ts build", - "preview": "bun ../src/cli.ts preview" + "dev": "bun ../src/cli/index.ts", + "build": "NODE_ENV=production bun ../src/cli/index.ts build", + "preview": "bun ../src/cli/index.ts preview" }, "dependencies": { "react": "^18.2.0", diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index d2e83884..00000000 --- a/src/cli.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env node -import { cac } from 'cac' - -import { version } from './version.js' - -const cli = cac('vocs') - -// dev -cli - .command('[root]') - .alias('dev') - .action(async () => { - const { createDevServer } = await import('./vite/dev-server.js') - const server = await createDevServer() - await server.listen() - server.printUrls() - }) - -// build -cli.command('build').action(async () => { - const { build } = await import('./vite/build.js') - await build() -}) - -// preview -cli.command('preview').action(async () => { - const { preview } = await import('./vite/preview.js') - const server = await preview() - server.printUrls() -}) - -cli.help() -cli.version(version) - -cli.parse() diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts new file mode 100644 index 00000000..901ae926 --- /dev/null +++ b/src/cli/commands/build.ts @@ -0,0 +1,4 @@ +export async function build() { + const { build } = await import('../../vite/build.js') + await build() +} diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts new file mode 100644 index 00000000..d9e5ff80 --- /dev/null +++ b/src/cli/commands/dev.ts @@ -0,0 +1,6 @@ +export async function dev() { + const { createDevServer } = await import('../../vite/dev-server.js') + const server = await createDevServer() + await server.listen() + server.printUrls() +} diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts new file mode 100644 index 00000000..8b81ef8a --- /dev/null +++ b/src/cli/commands/init.ts @@ -0,0 +1,92 @@ +// TODO: spice it up + +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +// @ts-expect-error +import { detect } from 'detect-package-manager' +import { execa } from 'execa' +import { default as fs } from 'fs-extra' +import { default as prompts } from 'prompts' + +type InitParameters = { name: string; git: boolean; install: boolean } + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +export async function init(options: InitParameters) { + const templateDir = resolve(__dirname, '../templates/default') + + const displayName = + options.name || + (( + await prompts({ + type: 'text', + name: 'displayName', + message: 'Enter the name of your project', + validate: (value) => (value ? true : 'Please enter a name'), + }) + ).displayName as string) + const name = kebabcase(displayName) as string + + const destDir = resolve(process.cwd(), name) + + console.log(`Scaffolding project in ${name}...`) + + // Copy contents + fs.copySync(templateDir, destDir) + + // Replace dotfiles + for (const file of fs.readdirSync(destDir)) { + if (!file.startsWith('_')) continue + fs.renameSync(resolve(destDir, file), resolve(destDir, `.${file.slice(1)}`)) + } + + // Replace package.json properties + const pkgJson = fs.readJsonSync(resolve(destDir, 'package.json')) + pkgJson.name = name + fs.writeJsonSync(resolve(destDir, 'package.json'), pkgJson, { spaces: 2 }) + + // Install dependencies + if (options.install) { + const packageManager = + typeof options.install === 'string' ? options.install : detectPackageManager() + await execa(packageManager, ['install', packageManager === 'npm' ? '--quiet' : '--silent'], { + cwd: destDir, + env: { + ...process.env, + ADBLOCK: '1', + DISABLE_OPENCOLLECTIVE: '1', + // we set NODE_ENV to development as pnpm skips dev + // dependencies when production + NODE_ENV: 'development', + }, + }) + } + + // Create git repository + if (options.git) { + await execa('git', ['init'], { cwd: destDir }) + await execa('git', ['add', '.'], { cwd: destDir }) + await execa('git', ['commit', '--no-verify', '--message', 'initial commit'], { + cwd: destDir, + }) + } + + console.log('Done!') +} + +export function kebabcase(str: string) { + return str + .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) + ?.join('-') + .toLowerCase() +} + +export function detectPackageManager() { + const userAgent = process.env.npm_config_user_agent + if (!userAgent) return 'npm' + if (userAgent.includes('pnpm')) return 'pnpm' + if (userAgent.includes('npm')) return 'npm' + if (userAgent.includes('yarn')) return 'yarn' + if (userAgent.includes('bun')) return 'bun' + return 'npm' +} diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts new file mode 100644 index 00000000..6c35146c --- /dev/null +++ b/src/cli/commands/preview.ts @@ -0,0 +1,5 @@ +export async function preview() { + const { preview } = await import('../../vite/preview.js') + const server = await preview() + server.printUrls() +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 00000000..4dc577f2 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { cac } from 'cac' + +import { build } from './commands/build.js' +import { dev } from './commands/dev.js' +import { init } from './commands/init.js' +import { preview } from './commands/preview.js' +import { version } from './version.js' + +const cli = cac('vocs') + +cli.command('[root]').alias('dev').action(dev) +cli + .command('init') + .option('-n, --name [name]', 'Name of project') + .option( + '-i, --install [false|npm|pnpm|yarn|bun]', + 'Install dependencies (and optionally force package manager)', + { + default: true, + }, + ) + .option('-g, --git', 'Initialize git repository', { default: true }) + .action(init) +cli.command('build').action(build) +cli.command('preview').action(preview) + +cli.help() +cli.version(version) + +cli.parse() diff --git a/src/cli/templates/default/README.md b/src/cli/templates/default/README.md new file mode 100644 index 00000000..3bb11a44 --- /dev/null +++ b/src/cli/templates/default/README.md @@ -0,0 +1 @@ +This is a [Vocs](https://vocs.dev) project bootstrapped with the Vocs CLI. diff --git a/src/cli/templates/default/_gitignore b/src/cli/templates/default/_gitignore new file mode 100644 index 00000000..7dc3ceb2 --- /dev/null +++ b/src/cli/templates/default/_gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# production +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# typescript +*.tsbuildinfo \ No newline at end of file diff --git a/src/cli/templates/default/docs/index.mdx b/src/cli/templates/default/docs/index.mdx new file mode 100644 index 00000000..6a1b6c6a --- /dev/null +++ b/src/cli/templates/default/docs/index.mdx @@ -0,0 +1,3 @@ +# Hello world! + +Welcome to my site! \ No newline at end of file diff --git a/src/cli/templates/default/package.json b/src/cli/templates/default/package.json new file mode 100644 index 00000000..d9435ae2 --- /dev/null +++ b/src/cli/templates/default/package.json @@ -0,0 +1,15 @@ +{ + "name": "__DOCS_NAME__", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vocs", + "build": "vocs build", + "preview": "vocs preview" + }, + "dependencies": { + "react": "latest", + "react-dom": "latest", + "vocs": "latest" + } +} diff --git a/src/cli/templates/default/tsconfig.json b/src/cli/templates/default/tsconfig.json new file mode 100644 index 00000000..d2636aac --- /dev/null +++ b/src/cli/templates/default/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/src/version.ts b/src/cli/version.ts similarity index 100% rename from src/version.ts rename to src/cli/version.ts diff --git a/src/package.json b/src/package.json index 2aa8ae0d..44e45627 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "type": "module", "bin": { - "vocs": "./_lib/cli.js" + "vocs": "./_lib/cli/index.js" }, "main": "./_lib/index.js", "exports": { @@ -31,6 +31,9 @@ "autoprefixer": "^10.4.16", "cac": "^6.7.14", "compression": "^1.7.4", + "detect-package-manager": "^3.0.1", + "execa": "^8.0.1", + "fs-extra": "^11.1.1", "globby": "^13.2.2", "hastscript": "^8.0.0", "mdast": "^3.0.0", @@ -39,6 +42,7 @@ "minimatch": "^9.0.3", "postcss": "^8.4.31", "postcss-prefix-selector": "^1.16.0", + "prompts": "^2.4.2", "react-helmet": "^6.1.0", "react-router-dom": "^6.16.0", "remark-directive": "^3.0.0", @@ -53,7 +57,9 @@ "vite": "4.4.11" }, "devDependencies": { + "@types/fs-extra": "^11.0.2", "@types/postcss-prefix-selector": "^1.16.2", + "@types/prompts": "^2.4.7", "@types/ua-parser-js": "^0.7.38" } } diff --git a/src/vite/plugins/css.ts b/src/vite/plugins/css.ts index 86c9297d..dabc697c 100644 --- a/src/vite/plugins/css.ts +++ b/src/vite/plugins/css.ts @@ -1,7 +1,7 @@ import { accessSync } from 'node:fs' import { resolve } from 'node:path' -import * as autoprefixer from 'autoprefixer' -import * as tailwindcss from 'tailwindcss' +import { default as autoprefixer } from 'autoprefixer' +import { default as tailwindcss } from 'tailwindcss' import type { PluginOption } from 'vite' import { postcssRawStyles } from './postcss/rawStyles.js' @@ -16,9 +16,9 @@ export function css(): PluginOption { postcss: { plugins: [ postcssRawStyles(), - (autoprefixer as any).default(), + autoprefixer(), tailwindConfig - ? tailwindcss.default({ + ? (tailwindcss as any)({ config: tailwindConfig, }) : undefined, diff --git a/src/vite/plugins/dev.ts b/src/vite/plugins/dev.ts index 0793ff80..5cd7b56f 100644 --- a/src/vite/plugins/dev.ts +++ b/src/vite/plugins/dev.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import * as serveStatic from 'serve-static' +import { default as serveStatic } from 'serve-static' import type { PluginOption } from 'vite' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -17,8 +17,7 @@ export function dev(): PluginOption { }, configureServer(server) { return () => { - // @ts-expect-error - server.middlewares.use(serveStatic.default(resolve(process.cwd(), 'public'))) + server.middlewares.use(serveStatic(resolve(process.cwd(), 'public'))) server.middlewares.use(async (req, res, next) => { const url = req.url && cleanUrl(req.url) diff --git a/src/vite/plugins/postcss/rawStyles.ts b/src/vite/plugins/postcss/rawStyles.ts index 88fa4568..a647d2a4 100644 --- a/src/vite/plugins/postcss/rawStyles.ts +++ b/src/vite/plugins/postcss/rawStyles.ts @@ -1,9 +1,7 @@ -// @ts-expect-error -import * as postcssPrefixSelector from 'postcss-prefix-selector' +import { default as postcssPrefixSelector } from 'postcss-prefix-selector' export function postcssRawStyles() { - // @ts-expect-error - return postcssPrefixSelector.default({ + return postcssPrefixSelector({ prefix: ':not([data-vocs-raw])', includeFiles: [/elements\.css/, /layouts\.css/], transform(prefix: string, selector_: string) { diff --git a/tsconfig.base.json b/tsconfig.base.json index c473eccb..d0cec300 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,7 +22,7 @@ // Interop constraints "esModuleInterop": false, - "allowSyntheticDefaultImports": false, + "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "verbatimModuleSyntax": true, "importHelpers": true, // This is only used for build validation. Since we do not have `tslib` installed, this will fail if we accidentally make use of anything that'd require injection of helpers.