diff --git a/package-lock.json b/package-lock.json index 12715b5..b090226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "workspaces": [ - "playgrounds/*", + "playground/*", "packages/*", "tooling/*" ], @@ -1137,10 +1137,6 @@ "resolved": "tooling/typescript", "link": true }, - "node_modules/@repo/ui": { - "resolved": "packages/ui", - "link": true - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", @@ -1963,6 +1959,11 @@ "node": ">=0.10.0" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1983,6 +1984,10 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/basic": { + "resolved": "playground/basic", + "link": true + }, "node_modules/better-path-resolve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", @@ -2656,6 +2661,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3382,6 +3401,33 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3821,6 +3867,10 @@ "node": ">= 0.4" } }, + "node_modules/head": { + "resolved": "playground/head", + "link": true + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4298,6 +4348,87 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -4391,6 +4522,11 @@ "node": ">=6" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5357,6 +5493,26 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5903,6 +6059,11 @@ "node": ">= 10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6017,14 +6178,6 @@ "node": ">=8" } }, - "node_modules/sonnet-class": { - "resolved": "playgrounds/sonnet-class", - "link": true - }, - "node_modules/sonnet-ssr": { - "resolved": "playgrounds/sonnet-ssr", - "link": true - }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -6138,6 +6291,10 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssr": { + "resolved": "playground/ssr", + "link": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7699,13 +7856,18 @@ "name": "create-sonnet-app", "version": "0.0.15", "license": "ISC", + "dependencies": { + "ejs": "^3.1.10", + "kolorist": "^1.8.0", + "prompts": "^2.4.2" + }, "bin": { "create-sonnet-app": "index.js" } }, "packages/sonnet-core": { "name": "@sonnetjs/core", - "version": "0.0.25", + "version": "0.0.27", "license": "MIT", "dependencies": { "@sonnetjs/shared": "*" @@ -7720,7 +7882,7 @@ }, "packages/sonnet-shared": { "name": "@sonnetjs/shared", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "devDependencies": { "@repo/eslint-config": "*", @@ -7745,6 +7907,7 @@ "packages/ui": { "name": "@repo/ui", "version": "0.0.0", + "extraneous": true, "license": "MIT", "devDependencies": { "@repo/eslint-config": "*", @@ -7753,7 +7916,7 @@ "typescript": "^5.3.3" } }, - "playgrounds/sonnet-class": { + "playground/basic": { "version": "0.0.1", "dependencies": { "@sonnetjs/core": "*" @@ -7763,10 +7926,10 @@ "vite": "^5.2.6" } }, - "playgrounds/sonnet-class/node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "playground/basic/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "dev": true, "optional": true, "peer": true, @@ -7774,7 +7937,7 @@ "undici-types": "~5.26.4" } }, - "playgrounds/sonnet-class/node_modules/vite": { + "playground/basic/node_modules/vite": { "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", @@ -7829,9 +7992,8 @@ } } }, - "playgrounds/sonnet-function": { + "playground/head": { "version": "0.0.1", - "extraneous": true, "dependencies": { "@sonnetjs/core": "*" }, @@ -7840,7 +8002,73 @@ "vite": "^5.2.6" } }, - "playgrounds/sonnet-ssr": { + "playground/head/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "playground/head/node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "playground/ssr": { "version": "0.0.0", "dependencies": { "@remix-run/router": "^1.15.3", @@ -7857,16 +8085,16 @@ "vite": "^5.0.10" } }, - "playgrounds/sonnet-ssr/node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "playground/ssr/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, - "playgrounds/sonnet-ssr/node_modules/vite": { + "playground/ssr/node_modules/vite": { "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", @@ -7921,6 +8149,46 @@ } } }, + "playgrounds/sonnet-class": { + "version": "0.0.1", + "extraneous": true, + "dependencies": { + "@sonnetjs/core": "*" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.2.6" + } + }, + "playgrounds/sonnet-function": { + "version": "0.0.1", + "extraneous": true, + "dependencies": { + "@sonnetjs/core": "*" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.2.6" + } + }, + "playgrounds/sonnet-ssr": { + "version": "0.0.0", + "extraneous": true, + "dependencies": { + "@remix-run/router": "^1.15.3", + "@sonnetjs/core": "*", + "compression": "^1.7.4", + "express": "^4.18.2", + "sirv": "^2.0.4" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.5", + "cross-env": "^7.0.3", + "typescript": "^5.3.3", + "vite": "^5.0.10" + } + }, "tooling/eslint": { "name": "@repo/eslint-config", "version": "0.0.0", diff --git a/packages/create-sonnet/createSonnet.js b/packages/create-sonnet/createSonnet.js deleted file mode 100644 index 40af356..0000000 --- a/packages/create-sonnet/createSonnet.js +++ /dev/null @@ -1,78 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -import * as readline from 'readline'; - -const createSonnet = async () => { - let appName = process.argv[2]; - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - if (!appName) { - await new Promise((resolve) => { - rl.question('What is the name of your app? ', (answer) => { - appName = answer; - resolve(); - }); - }); - } - - if (!appName) { - console.error('Please provide an app name.'); - rl.close(); - return; - } - - console.log('Creating a new Sonnet app...'); - - const examplePath = path.resolve(__dirname, 'templates/basic'); - - const targetPath = path.resolve(process.cwd(), appName); - - fs.mkdirSync(targetPath); - - const copy = (src, dest) => { - const entries = fs.readdirSync(src); - - for (const entry of entries) { - const srcPath = path.join(src, entry); - const destPath = path.join(dest, entry); - - if (fs.lstatSync(srcPath).isDirectory()) { - fs.mkdirSync(destPath); - copy(srcPath, destPath); - } else { - fs.copyFileSync(srcPath, destPath); - } - } - }; - - copy(examplePath, targetPath); - - console.log(`Created a new Sonnet app at ${targetPath}`); - - console.log('To get started, run the following commands:'); - - console.log(''); - - if (appName !== '.') { - console.log('cd', appName); - } - console.log('npm install'); - console.log('npm run dev'); - - console.log(''); - - console.log('Done!'); - rl.close(); -}; - -export { createSonnet }; diff --git a/packages/create-sonnet/credits.txt b/packages/create-sonnet/credits.txt new file mode 100644 index 0000000..7d65fdf --- /dev/null +++ b/packages/create-sonnet/credits.txt @@ -0,0 +1,8 @@ +================================================================= +Credits: +================================================================= + +- The original author of this mod is [Haoqun Jiang](https://github.com/sodatea) +- The code is extracted from the [original repository](https://github.com/vuejs/create-vue) +- All the credits go to the original author and contributors of the original repository +- The original code is licensed under the [MIT License](https://github.com/vuejs/create-vue/blob/main/LICENSE) diff --git a/packages/create-sonnet/index.js b/packages/create-sonnet/index.js old mode 100755 new mode 100644 index 92f215c..1c30cf4 --- a/packages/create-sonnet/index.js +++ b/packages/create-sonnet/index.js @@ -1,5 +1,313 @@ #!/usr/bin/env node -import { createSonnet } from './createSonnet.js'; +import * as fs from 'node:fs' +import * as path from 'node:path' -createSonnet(); +import { parseArgs } from 'node:util' + +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +import ejs from 'ejs' + +import prompts from 'prompts' +import { red, green, bold } from 'kolorist' + +import renderTemplate from './utils/renderTemplate.js' +import { postOrderDirectoryTraverse, preOrderDirectoryTraverse } from './utils/directoryTraverse.js' +import getCommand from './utils/getCommand.js' + +function isValidPackageName(projectName) { + return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName) +} + +function toValidPackageName(projectName) { + return projectName + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/^[._]/, '') + .replace(/[^a-z0-9-~]+/g, '-') +} + +function canSkipEmptying(dir) { + if (!fs.existsSync(dir)) { + return true + } + + const files = fs.readdirSync(dir) + if (files.length === 0) { + return true + } + if (files.length === 1 && files[0] === '.git') { + return true + } + + return false +} + +function emptyDir(dir) { + if (!fs.existsSync(dir)) { + return + } + + postOrderDirectoryTraverse( + dir, + (dir) => fs.rmdirSync(dir), + (file) => fs.unlinkSync(file) + ) +} + +async function init() { + console.log() + console.log( + bold('Sonnet JS: A frontend framework') + ) + console.log() + + const cwd = process.cwd() + // possible options: + // --default + // --typescript / --ts + // --sonnet-ssr / --ssr + // --sonnet-router / --router + + const args = process.argv.slice(2) + + // alias is not supported by parseArgs + const options = { + typescript: { type: 'boolean' }, + ts: { type: 'boolean' }, + 'sonnet-router': { type: 'boolean' }, + router: { type: 'boolean' }, + 'sonnet-ssr': { type: 'boolean' }, + ssr: { type: 'boolean' } + } + + const { values: argv, positionals } = parseArgs({ + args, + options, + strict: false + }) + + // if any of the feature flags is set, we would skip the feature prompts + const isFeatureFlagsUsed = + typeof ( + argv.default ?? + (argv.ts || argv.typescript) ?? + (argv.router || argv['sonnet-router']) ?? + (argv.ssr || argv['sonnet-ssr']) + ) === 'boolean' + + let targetDir = positionals[0] + const defaultProjectName = !targetDir ? 'sonnet-project' : targetDir + + const forceOverwrite = argv.force + + const language = { + "projectName": { + "message": "Project name:" + }, + "shouldOverwrite": { + "dirForPrompts": { + "current": "Current directory", + "target": "Target directory" + }, + "message": "is not empty. Remove existing files and continue?" + }, + "packageName": { + "message": "Package name:", + "invalidMessage": "Invalid package.json name" + }, + "needsTypeScript": { + "message": "Add TypeScript?" + }, + "errors": { + "operationCancelled": "Operation cancelled" + }, + "defaultToggleOptions": { + "active": "Yes", + "inactive": "No" + }, + "infos": { + "done": "Done. Now run:" + } + } + /** + * @type {Record} + */ + let result = {} + + try { + // Prompts: + // - Project name: + // - whether to overwrite the existing directory or not? + // - enter a valid package name for package.json + // - Project language: JavaScript / TypeScript + // - Install Sonnet Router for SPA development? + // - Install Sonnet SSR for development? + result = await prompts( + [ + { + name: 'projectName', + type: targetDir ? null : 'text', + message: language.projectName.message, + initial: defaultProjectName, + onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName) + }, + { + name: 'shouldOverwrite', + type: () => (canSkipEmptying(targetDir) || forceOverwrite ? null : 'toggle'), + message: () => { + const dirForPrompt = + targetDir === '.' + ? language.shouldOverwrite.dirForPrompts.current + : `${language.shouldOverwrite.dirForPrompts.target} "${targetDir}"` + + return `${dirForPrompt} ${language.shouldOverwrite.message}` + }, + initial: true, + active: language.defaultToggleOptions.active, + inactive: language.defaultToggleOptions.inactive + }, + { + name: 'overwriteChecker', + type: (prev, values) => { + if (values.shouldOverwrite === false) { + throw new Error(red('✖') + ` ${language.errors.operationCancelled}`) + } + return null + } + }, + { + name: 'packageName', + type: () => (isValidPackageName(targetDir) ? null : 'text'), + message: language.packageName.message, + initial: () => toValidPackageName(targetDir), + validate: (dir) => isValidPackageName(dir) || language.packageName.invalidMessage + }, + { + name: 'selectTemplate', + type: (prev, values) => { + return 'select'; + }, + message: 'Select a template', + choices: [ + { title: 'Default', value: 'default' }, + { title: 'Router', value: 'router' }, + { title: 'SSR', value: 'ssr' }, + ], + initial: 0, + }, + { + name: 'needsTypeScript', + type: () => (isFeatureFlagsUsed ? null : 'toggle'), + message: language.needsTypeScript.message, + initial: false, + active: language.defaultToggleOptions.active, + inactive: language.defaultToggleOptions.inactive + }, + ], + { + onCancel: () => { + throw new Error(red('✖') + ` ${language.errors.operationCancelled}`) + } + } + ) + } catch (cancelled) { + console.log(cancelled.message) + process.exit(1) + } + + // `initial` won't take effect if the prompt type is null + // so we still have to assign the default values here + const { + projectName, + packageName = projectName ?? defaultProjectName, + shouldOverwrite = argv.force, + needsTypeScript = argv.ts || argv.typescript, + } = result + + const root = path.join(cwd, targetDir) + + if (fs.existsSync(root) && shouldOverwrite) { + emptyDir(root) + } else if (!fs.existsSync(root)) { + fs.mkdirSync(root) + } + + const pkg = { name: packageName, version: '0.0.0' } + fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2)) + + const templateRoot = path.resolve(__dirname, 'template') + + /** + * @type Array<(dataStore: Record) => Promise> + */ + const callbacks = [] + const render = function render(templateName) { + const templateDir = path.resolve(templateRoot, templateName) + renderTemplate(templateDir, root, callbacks) + } + // Render base template + render('base') + + // Render code template. + // prettier-ignore + const codeTemplate = + (result.selectTemplate) + + (needsTypeScript ? '-ts' : '') + render(`code/${codeTemplate}`) + + // An external data store for callbacks to share data + const dataStore = {} + // Process callbacks + for (const cb of callbacks) { + await cb(dataStore) + } + + // EJS template rendering + preOrderDirectoryTraverse( + root, + () => {}, + (filepath) => { + if (filepath.endsWith('.ejs')) { + const template = fs.readFileSync(filepath, 'utf-8') + const dest = filepath.replace(/\.ejs$/, '') + const content = ejs.render(template, dataStore[dest]) + + fs.writeFileSync(dest, content) + fs.unlinkSync(filepath) + } + } + ) + + // Instructions: + // Supported package managers: pnpm > yarn > bun > npm + const userAgent = process.env.npm_config_user_agent ?? '' + const packageManager = /pnpm/.test(userAgent) + ? 'pnpm' + : /yarn/.test(userAgent) + ? 'yarn' + : /bun/.test(userAgent) + ? 'bun' + : 'npm' + + console.log(`\n${language.infos.done}\n`) + if (root !== cwd) { + const cdProjectName = path.relative(cwd, root) + console.log( + ` ${bold(green(`cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`))}` + ) + } + console.log(` ${bold(green(getCommand(packageManager, 'install')))}`) + console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`) + console.log() +} + +init().catch((e) => { + console.error(e) +}) \ No newline at end of file diff --git a/packages/create-sonnet/package.json b/packages/create-sonnet/package.json index 0738e1c..cca6c88 100644 --- a/packages/create-sonnet/package.json +++ b/packages/create-sonnet/package.json @@ -1,6 +1,6 @@ { "name": "create-sonnet-app", - "version": "0.0.15", + "version": "0.0.16", "description": "", "main": "index.js", "type": "module", @@ -12,5 +12,10 @@ "license": "ISC", "publishConfig": { "access": "public" + }, + "dependencies": { + "ejs": "^3.1.10", + "kolorist": "^1.8.0", + "prompts": "^2.4.2" } } \ No newline at end of file diff --git a/packages/create-sonnet/template/base/_gitignore b/packages/create-sonnet/template/base/_gitignore new file mode 100644 index 0000000..54f07af --- /dev/null +++ b/packages/create-sonnet/template/base/_gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/packages/create-sonnet/template/base/package.json b/packages/create-sonnet/template/base/package.json new file mode 100644 index 0000000..54f9d09 --- /dev/null +++ b/packages/create-sonnet/template/base/package.json @@ -0,0 +1,6 @@ +{ + "name": "basic", + "private": true, + "version": "0.0.1", + "type": "module" +} \ No newline at end of file diff --git a/packages/create-sonnet/templates/basic/public/vite.svg b/packages/create-sonnet/template/base/public/vite.svg similarity index 100% rename from packages/create-sonnet/templates/basic/public/vite.svg rename to packages/create-sonnet/template/base/public/vite.svg diff --git a/packages/create-sonnet/templates/basic/src/style.css b/packages/create-sonnet/template/base/src/style.css similarity index 99% rename from packages/create-sonnet/templates/basic/src/style.css rename to packages/create-sonnet/template/base/src/style.css index f9c7350..f047de6 100644 --- a/packages/create-sonnet/templates/basic/src/style.css +++ b/packages/create-sonnet/template/base/src/style.css @@ -93,4 +93,4 @@ button:focus-visible { button { background-color: #f9f9f9; } -} +} \ No newline at end of file diff --git a/packages/create-sonnet/templates/basic/index.html b/packages/create-sonnet/template/code/default-ts/index.html similarity index 97% rename from packages/create-sonnet/templates/basic/index.html rename to packages/create-sonnet/template/code/default-ts/index.html index 44a9335..51542af 100644 --- a/packages/create-sonnet/templates/basic/index.html +++ b/packages/create-sonnet/template/code/default-ts/index.html @@ -10,4 +10,4 @@
- + \ No newline at end of file diff --git a/packages/create-sonnet/templates/basic/package.json b/packages/create-sonnet/template/code/default-ts/package.json similarity index 50% rename from packages/create-sonnet/templates/basic/package.json rename to packages/create-sonnet/template/code/default-ts/package.json index d13a13e..7698939 100644 --- a/packages/create-sonnet/templates/basic/package.json +++ b/packages/create-sonnet/template/code/default-ts/package.json @@ -1,19 +1,14 @@ { - "name": "c-vite", - "private": true, - "version": "0.0.1", - "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "devDependencies": { - "typescript": "^5.2.2", + "typescript": "^5.2.6", "vite": "^5.2.6" }, "dependencies": { - "@sonnetjs/core": "^0.0.16", - "@sonnetjs/html": "^0.0.1" + "@sonnetjs/core": "0.0.27" } } \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default-ts/src/App.ts b/packages/create-sonnet/template/code/default-ts/src/App.ts new file mode 100644 index 0000000..127aadf --- /dev/null +++ b/packages/create-sonnet/template/code/default-ts/src/App.ts @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import Counter from './Counter'; + +class App extends SonnetComponent { + get() { + return /*html*/ ` +
+ ${Counter().get()} +
+ `; + } +} + +export default $component(App); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default-ts/src/Counter.ts b/packages/create-sonnet/template/code/default-ts/src/Counter.ts new file mode 100644 index 0000000..32de1ec --- /dev/null +++ b/packages/create-sonnet/template/code/default-ts/src/Counter.ts @@ -0,0 +1,32 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Counter extends SonnetComponent { + counter = 0; + + script() { + const counterButton = document.getElementById( + 'counter', + ) as HTMLButtonElement | null; + counterButton?.addEventListener('click', () => { + this.counter += 1; + counterButton.innerText = `count is ${this.counter}`; + }); + } + + get() { + return /*html*/ ` +
+ + + +

Vite

+
+ +
+

Edit src/main.ts and save to test HMR.

+
+ `; + } +} + +export default $component(Counter); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default-ts/src/main.ts b/packages/create-sonnet/template/code/default-ts/src/main.ts new file mode 100644 index 0000000..136adaf --- /dev/null +++ b/packages/create-sonnet/template/code/default-ts/src/main.ts @@ -0,0 +1,7 @@ +import './style.css'; +import { createApp } from '@sonnetjs/core'; +import App from './App'; + +const app = createApp(); +app.root(App); +app.mount('#app'); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default/index.html b/packages/create-sonnet/template/code/default/index.html new file mode 100644 index 0000000..bc56a6c --- /dev/null +++ b/packages/create-sonnet/template/code/default/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + TS + + +
+ + + \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default/package.json b/packages/create-sonnet/template/code/default/package.json new file mode 100644 index 0000000..70b13bb --- /dev/null +++ b/packages/create-sonnet/template/code/default/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.2.6" + }, + "dependencies": { + "@sonnetjs/core": "0.0.27" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default/src/App.js b/packages/create-sonnet/template/code/default/src/App.js new file mode 100644 index 0000000..127aadf --- /dev/null +++ b/packages/create-sonnet/template/code/default/src/App.js @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import Counter from './Counter'; + +class App extends SonnetComponent { + get() { + return /*html*/ ` +
+ ${Counter().get()} +
+ `; + } +} + +export default $component(App); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default/src/Counter.js b/packages/create-sonnet/template/code/default/src/Counter.js new file mode 100644 index 0000000..a0c357b --- /dev/null +++ b/packages/create-sonnet/template/code/default/src/Counter.js @@ -0,0 +1,32 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Counter extends SonnetComponent { + counter = 0; + + script() { + const counterButton = document.getElementById( + 'counter', + ); + counterButton?.addEventListener('click', () => { + this.counter += 1; + counterButton.innerText = `count is ${this.counter}`; + }); + } + + get() { + return /*html*/ ` +
+ + + +

Vite

+
+ +
+

Edit src/main.ts and save to test HMR.

+
+ `; + } +} + +export default $component(Counter); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/default/src/main.js b/packages/create-sonnet/template/code/default/src/main.js new file mode 100644 index 0000000..136adaf --- /dev/null +++ b/packages/create-sonnet/template/code/default/src/main.js @@ -0,0 +1,7 @@ +import './style.css'; +import { createApp } from '@sonnetjs/core'; +import App from './App'; + +const app = createApp(); +app.root(App); +app.mount('#app'); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/index.html b/packages/create-sonnet/template/code/router-ts/index.html new file mode 100644 index 0000000..51542af --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + TS + + +
+ + + \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/package.json b/packages/create-sonnet/template/code/router-ts/package.json new file mode 100644 index 0000000..d1711ae --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/package.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.2.6", + "vite": "^5.2.6" + }, + "dependencies": { + "@sonnetjs/core": "0.0.27", + "@sonnetjs/router": "0.0.3" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/App.ts b/packages/create-sonnet/template/code/router-ts/src/App.ts new file mode 100644 index 0000000..9fd53c6 --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/App.ts @@ -0,0 +1,19 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import { Header } from './partials/Header'; + +class App extends SonnetComponent { + static script(): void { + console.log('App script'); + } + + public get() { + return /*html */ ` + ${Header()} +
+ ${this.children()} +
+

About

+

Welcome to the Sonnet Playground

+ + `; + } +} + +export default $component(About); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/pages/Contact.ts b/packages/create-sonnet/template/code/router-ts/src/pages/Contact.ts new file mode 100644 index 0000000..4cc5646 --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/pages/Contact.ts @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Contact extends SonnetComponent { + public get() { + return /*html */ ` +
+

Contact

+

Welcome to the Sonnet Playground

+
+ `; + } +} + +export default $component(Contact); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/pages/Home.ts b/packages/create-sonnet/template/code/router-ts/src/pages/Home.ts new file mode 100644 index 0000000..62ac9bf --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/pages/Home.ts @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Home extends SonnetComponent { + public get() { + return /*html */ ` +
+

Home

+

Welcome to the Sonnet Playground

+
+ `; + } +} + +export default $component(Home); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/partials/Header.ts b/packages/create-sonnet/template/code/router-ts/src/partials/Header.ts new file mode 100644 index 0000000..302efc4 --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/partials/Header.ts @@ -0,0 +1,10 @@ +export const Header = () => /*html*/ ` +
+

My Website

+ +
+ `; \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/partials/Layout.ts b/packages/create-sonnet/template/code/router-ts/src/partials/Layout.ts new file mode 100644 index 0000000..599b87a --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/partials/Layout.ts @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class RootComponent extends SonnetComponent { + public get() { + return /*html*/ ` +
+ ${this.children()} + this is new thing +
+ `; + } +} + +export default $component(RootComponent); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router-ts/src/router.ts b/packages/create-sonnet/template/code/router-ts/src/router.ts new file mode 100644 index 0000000..13110a7 --- /dev/null +++ b/packages/create-sonnet/template/code/router-ts/src/router.ts @@ -0,0 +1,33 @@ +import { + RouteObject, + createBrowserHistory, + createRouter, +} from '@sonnetjs/router'; + +const routes: RouteObject[] = [ + { + rootComponent: async () => (await import('./partials/Layout')).default(), + children: [ + { + path: '/', + component: async () => (await import('./pages/Home')).default(), + }, + { + path: '/about', + component: async () => (await import('./pages/About')).default(), + }, + { + path: '/contact', + component: async () => (await import('./pages/Contact')).default(), + }, + ], + }, +]; + +const history = createBrowserHistory(); + +export const router = createRouter({ + routes, + history, + mountedId: '#app-1', +}); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/index.html b/packages/create-sonnet/template/code/router/index.html new file mode 100644 index 0000000..bc56a6c --- /dev/null +++ b/packages/create-sonnet/template/code/router/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + TS + + +
+ + + \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/package copy.json b/packages/create-sonnet/template/code/router/package copy.json new file mode 100644 index 0000000..d1711ae --- /dev/null +++ b/packages/create-sonnet/template/code/router/package copy.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.2.6", + "vite": "^5.2.6" + }, + "dependencies": { + "@sonnetjs/core": "0.0.27", + "@sonnetjs/router": "0.0.3" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/package.json b/packages/create-sonnet/template/code/router/package.json new file mode 100644 index 0000000..c86a2ab --- /dev/null +++ b/packages/create-sonnet/template/code/router/package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.2.6" + }, + "dependencies": { + "@sonnetjs/core": "0.0.27", + "@sonnetjs/router": "0.0.3" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/App.js b/packages/create-sonnet/template/code/router/src/App.js new file mode 100644 index 0000000..e47138c --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/App.js @@ -0,0 +1,19 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import { Header } from './partials/Header'; + +class App extends SonnetComponent { + static script() { + console.log('App script'); + } + + get() { + return /*html */ ` + ${Header()} +
+ ${this.children_} +
+

About

+

Welcome to the Sonnet Playground

+ + `; + } +} + +export default $component(About); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/pages/Contact.js b/packages/create-sonnet/template/code/router/src/pages/Contact.js new file mode 100644 index 0000000..a70ab59 --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/pages/Contact.js @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Contact extends SonnetComponent { + get() { + return /*html */ ` +
+

Contact

+

Welcome to the Sonnet Playground

+
+ `; + } +} + +export default $component(Contact); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/pages/Home.js b/packages/create-sonnet/template/code/router/src/pages/Home.js new file mode 100644 index 0000000..de49bd9 --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/pages/Home.js @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Home extends SonnetComponent { + get() { + return /*html */ ` +
+

Home

+

Welcome to the Sonnet Playground

+
+ `; + } +} + +export default $component(Home); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/partials/Header.js b/packages/create-sonnet/template/code/router/src/partials/Header.js new file mode 100644 index 0000000..302efc4 --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/partials/Header.js @@ -0,0 +1,10 @@ +export const Header = () => /*html*/ ` +
+

My Website

+ +
+ `; \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/partials/Layout.js b/packages/create-sonnet/template/code/router/src/partials/Layout.js new file mode 100644 index 0000000..7a8b72d --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/partials/Layout.js @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class RootComponent extends SonnetComponent { + get() { + return /*html*/ ` +
+ ${this.children_} + this is new thing +
+ `; + } +} + +export default $component(RootComponent); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/router/src/router.js b/packages/create-sonnet/template/code/router/src/router.js new file mode 100644 index 0000000..b270a90 --- /dev/null +++ b/packages/create-sonnet/template/code/router/src/router.js @@ -0,0 +1,32 @@ +import { + createBrowserHistory, + createRouter, +} from '@sonnetjs/router'; + +const routes = [ + { + rootComponent: async () => (await import('./partials/Layout')).default(), + children: [ + { + path: '/', + component: async () => (await import('./pages/Home')).default(), + }, + { + path: '/about', + component: async () => (await import('./pages/About')).default(), + }, + { + path: '/contact', + component: async () => (await import('./pages/Contact')).default(), + }, + ], + }, +]; + +const history = createBrowserHistory(); + +export const router = createRouter({ + routes, + history, + mountedId: '#app-1', +}); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/index.html b/packages/create-sonnet/template/code/ssr-ts/index.html new file mode 100644 index 0000000..acdf270 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/index.html @@ -0,0 +1,14 @@ + + + + + + + Vite + TS + + + +
+ + + \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/package.json b/packages/create-sonnet/template/code/ssr-ts/package.json new file mode 100644 index 0000000..8f51d36 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "dev": "node server", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build --ssrManifest --outDir dist/client", + "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server", + "preview": "cross-env NODE_ENV=production node server" + }, + "dependencies": { + "compression": "^1.7.4", + "express": "^4.18.2", + "sirv": "^2.0.4", + "@sonnetjs/core": "*" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "cross-env": "^7.0.3", + "vite": "^5.0.10" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/server.js b/packages/create-sonnet/template/code/ssr-ts/server.js new file mode 100644 index 0000000..cd2fa9e --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/server.js @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import express from 'express'; + +// Constants +const isProduction = process.env.NODE_ENV === 'production'; +const port = process.env.PORT || 5174; +const base = process.env.BASE || '/'; + +// Cached production assets +const templateHtml = isProduction + ? await fs.readFile('./dist/client/index.html', 'utf-8') + : ''; +const ssrManifest = isProduction + ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') + : undefined; + +// Create http server +const app = express(); + +// Add Vite or respective production middlewares +let vite; +if (!isProduction) { + const { createServer } = await import('vite'); + vite = await createServer({ + server: { middlewareMode: true, watch: { usePolling: true } }, + appType: 'custom', + base, + }); + app.use(vite.middlewares); +} else { + const compression = (await import('compression')).default; + const sirv = (await import('sirv')).default; + app.use(compression()); + app.use(base, sirv('./dist/client', { extensions: [] })); +} + +// Serve HTML +app.use('*', async (req, res) => { + try { + const url = req.originalUrl.replace(base, ''); + + let template; + let render; + if (!isProduction) { + // Always read fresh template in development + template = await fs.readFile('./index.html', 'utf-8'); + template = await vite.transformIndexHtml(url, template); + render = (await vite.ssrLoadModule('/src/entry-server.ts')).render; + } else { + template = templateHtml; + render = (await import('./dist/server/entry-server.js')).render; + } + + const rendered = await render(url, ssrManifest); + + const html = template + .replace(``, rendered.head ?? '') + .replace(``, rendered.html ?? ''); + + res.status(200).set({ 'Content-Type': 'text/html' }).send(html); + } catch (e) { + vite?.ssrFixStacktrace(e); + console.log(e.stack); + res.status(500).end(e.stack); + } +}); + +app.get('/', (req, res) => { + res.redirect(301, '/home'); +}); + +// Start http server +app.listen(port, () => { + console.log(`Server started at http://localhost:${port}`); +}); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/src/App.ts b/packages/create-sonnet/template/code/ssr-ts/src/App.ts new file mode 100644 index 0000000..127aadf --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/src/App.ts @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import Counter from './Counter'; + +class App extends SonnetComponent { + get() { + return /*html*/ ` +
+ ${Counter().get()} +
+ `; + } +} + +export default $component(App); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/src/Counter.ts b/packages/create-sonnet/template/code/ssr-ts/src/Counter.ts new file mode 100644 index 0000000..32de1ec --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/src/Counter.ts @@ -0,0 +1,32 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Counter extends SonnetComponent { + counter = 0; + + script() { + const counterButton = document.getElementById( + 'counter', + ) as HTMLButtonElement | null; + counterButton?.addEventListener('click', () => { + this.counter += 1; + counterButton.innerText = `count is ${this.counter}`; + }); + } + + get() { + return /*html*/ ` +
+ + + +

Vite

+
+ +
+

Edit src/main.ts and save to test HMR.

+
+ `; + } +} + +export default $component(Counter); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/src/entry-client.ts b/packages/create-sonnet/template/code/ssr-ts/src/entry-client.ts new file mode 100644 index 0000000..3860318 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/src/entry-client.ts @@ -0,0 +1,9 @@ +import './style.css'; +import { createApp } from '@sonnetjs/core'; + +import App from './App'; + +const app = createApp(); +app.root(App); +app.ssr(); +app.mount('#app'); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr-ts/src/entry-server.ts b/packages/create-sonnet/template/code/ssr-ts/src/entry-server.ts new file mode 100644 index 0000000..cc0eaa3 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr-ts/src/entry-server.ts @@ -0,0 +1,6 @@ +import App from './App'; + +export async function render() { + const html = await App().get(); + return { html }; +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/index.html b/packages/create-sonnet/template/code/ssr/index.html new file mode 100644 index 0000000..e306772 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/index.html @@ -0,0 +1,14 @@ + + + + + + + Vite + TS + + + +
+ + + \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/package.json b/packages/create-sonnet/template/code/ssr/package.json new file mode 100644 index 0000000..efb4afe --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "dev": "node server", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build --ssrManifest --outDir dist/client", + "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server", + "preview": "cross-env NODE_ENV=production node server" + }, + "dependencies": { + "compression": "^1.7.4", + "express": "^4.18.2", + "sirv": "^2.0.4", + "@sonnetjs/core": "*" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.5", + "cross-env": "^7.0.3", + "typescript": "^5.3.3", + "vite": "^5.0.10" + } +} \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/server.js b/packages/create-sonnet/template/code/ssr/server.js new file mode 100644 index 0000000..8ec8a57 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/server.js @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import express from 'express'; + +// Constants +const isProduction = process.env.NODE_ENV === 'production'; +const port = process.env.PORT || 5174; +const base = process.env.BASE || '/'; + +// Cached production assets +const templateHtml = isProduction + ? await fs.readFile('./dist/client/index.html', 'utf-8') + : ''; +const ssrManifest = isProduction + ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') + : undefined; + +// Create http server +const app = express(); + +// Add Vite or respective production middlewares +let vite; +if (!isProduction) { + const { createServer } = await import('vite'); + vite = await createServer({ + server: { middlewareMode: true, watch: { usePolling: true } }, + appType: 'custom', + base, + }); + app.use(vite.middlewares); +} else { + const compression = (await import('compression')).default; + const sirv = (await import('sirv')).default; + app.use(compression()); + app.use(base, sirv('./dist/client', { extensions: [] })); +} + +// Serve HTML +app.use('*', async (req, res) => { + try { + const url = req.originalUrl.replace(base, ''); + + let template; + let render; + if (!isProduction) { + // Always read fresh template in development + template = await fs.readFile('./index.html', 'utf-8'); + template = await vite.transformIndexHtml(url, template); + render = (await vite.ssrLoadModule('/src/entry-server.js')).render; + } else { + template = templateHtml; + render = (await import('./dist/server/entry-server.js')).render; + } + + const rendered = await render(url, ssrManifest); + + const html = template + .replace(``, rendered.head ?? '') + .replace(``, rendered.html ?? ''); + + res.status(200).set({ 'Content-Type': 'text/html' }).send(html); + } catch (e) { + vite?.ssrFixStacktrace(e); + console.log(e.stack); + res.status(500).end(e.stack); + } +}); + +app.get('/', (req, res) => { + res.redirect(301, '/home'); +}); + +// Start http server +app.listen(port, () => { + console.log(`Server started at http://localhost:${port}`); +}); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/src/App.js b/packages/create-sonnet/template/code/ssr/src/App.js new file mode 100644 index 0000000..127aadf --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/src/App.js @@ -0,0 +1,14 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; +import Counter from './Counter'; + +class App extends SonnetComponent { + get() { + return /*html*/ ` +
+ ${Counter().get()} +
+ `; + } +} + +export default $component(App); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/src/Counter.js b/packages/create-sonnet/template/code/ssr/src/Counter.js new file mode 100644 index 0000000..a0c357b --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/src/Counter.js @@ -0,0 +1,32 @@ +import { $component, SonnetComponent } from '@sonnetjs/core'; + +class Counter extends SonnetComponent { + counter = 0; + + script() { + const counterButton = document.getElementById( + 'counter', + ); + counterButton?.addEventListener('click', () => { + this.counter += 1; + counterButton.innerText = `count is ${this.counter}`; + }); + } + + get() { + return /*html*/ ` +
+ + + +

Vite

+
+ +
+

Edit src/main.ts and save to test HMR.

+
+ `; + } +} + +export default $component(Counter); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/src/entry-client.js b/packages/create-sonnet/template/code/ssr/src/entry-client.js new file mode 100644 index 0000000..3860318 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/src/entry-client.js @@ -0,0 +1,9 @@ +import './style.css'; +import { createApp } from '@sonnetjs/core'; + +import App from './App'; + +const app = createApp(); +app.root(App); +app.ssr(); +app.mount('#app'); \ No newline at end of file diff --git a/packages/create-sonnet/template/code/ssr/src/entry-server.js b/packages/create-sonnet/template/code/ssr/src/entry-server.js new file mode 100644 index 0000000..cc0eaa3 --- /dev/null +++ b/packages/create-sonnet/template/code/ssr/src/entry-server.js @@ -0,0 +1,6 @@ +import App from './App'; + +export async function render() { + const html = await App().get(); + return { html }; +} \ No newline at end of file diff --git a/packages/create-sonnet/templates/basic/src/Counter.ts b/packages/create-sonnet/templates/basic/src/Counter.ts deleted file mode 100644 index e458fc5..0000000 --- a/packages/create-sonnet/templates/basic/src/Counter.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { $component, SonnetComponent } from '@sonnetjs/core'; -import { a, button, div, h1, img, p } from '@sonnetjs/html'; - -class Counter extends SonnetComponent { - counter = 0; - - public script() { - const counterButton = document.getElementById( - 'counter', - ) as HTMLButtonElement; - counterButton.addEventListener('click', () => { - this.counter += 1; - counterButton.innerText = `count is ${this.counter}`; - }); - } - - public get() { - return div() - .children( - a() - .href('https://vitejs.dev') - .target('blank') - .children( - img() - .src('https://vitejs.dev/logo.svg') - .className('logo') - .alt('Vite Logo') - .get(), - ) - .get(), - h1().innerText('Vite').get(), - div() - .className('card') - .children( - button() - .id('counter') - .type('button') - .innerText(`count is ${this.counter}`) - .className('btn') - .get(), - ) - .get(), - p() - .innerText('Edit src/main.ts and save to test HMR.') - .className('read-the-docs') - .get(), - ) - .get(); - } -} - -export default $component(Counter); diff --git a/packages/create-sonnet/templates/basic/src/main.ts b/packages/create-sonnet/templates/basic/src/main.ts deleted file mode 100644 index 3c6a9cb..0000000 --- a/packages/create-sonnet/templates/basic/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './style.css'; -import { createApp } from '@sonnetjs/core'; -import Counter from './Counter'; - -const app = createApp(Counter()); -app.mount('#app'); diff --git a/packages/create-sonnet/templates/basic/src/typescript.svg b/packages/create-sonnet/templates/basic/src/typescript.svg deleted file mode 100644 index d91c910..0000000 --- a/packages/create-sonnet/templates/basic/src/typescript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-sonnet/templates/basic/src/vite-env.d.ts b/packages/create-sonnet/templates/basic/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/packages/create-sonnet/templates/basic/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/create-sonnet/templates/basic/tsconfig.json b/packages/create-sonnet/templates/basic/tsconfig.json deleted file mode 100644 index 75abdef..0000000 --- a/packages/create-sonnet/templates/basic/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/packages/create-sonnet/utils/deepMerge.js b/packages/create-sonnet/utils/deepMerge.js new file mode 100644 index 0000000..fced189 --- /dev/null +++ b/packages/create-sonnet/utils/deepMerge.js @@ -0,0 +1,26 @@ +const isObject = (val) => val && typeof val === 'object' +const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b])) + +/** + * Recursively merge the content of the new object to the existing one + * @param {Object} target the existing object + * @param {Object} obj the new object + */ +function deepMerge(target, obj) { + for (const key of Object.keys(obj)) { + const oldVal = target[key] + const newVal = obj[key] + + if (Array.isArray(oldVal) && Array.isArray(newVal)) { + target[key] = mergeArrayWithDedupe(oldVal, newVal) + } else if (isObject(oldVal) && isObject(newVal)) { + target[key] = deepMerge(oldVal, newVal) + } else { + target[key] = newVal + } + } + + return target +} + +export default deepMerge \ No newline at end of file diff --git a/packages/create-sonnet/utils/directoryTraverse.js b/packages/create-sonnet/utils/directoryTraverse.js new file mode 100644 index 0000000..6530bb2 --- /dev/null +++ b/packages/create-sonnet/utils/directoryTraverse.js @@ -0,0 +1,35 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' + +export function preOrderDirectoryTraverse(dir, dirCallback, fileCallback) { + for (const filename of fs.readdirSync(dir)) { + if (filename === '.git') { + continue + } + const fullpath = path.resolve(dir, filename) + if (fs.lstatSync(fullpath).isDirectory()) { + dirCallback(fullpath) + // in case the dirCallback removes the directory entirely + if (fs.existsSync(fullpath)) { + preOrderDirectoryTraverse(fullpath, dirCallback, fileCallback) + } + continue + } + fileCallback(fullpath) + } +} + +export function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) { + for (const filename of fs.readdirSync(dir)) { + if (filename === '.git') { + continue + } + const fullpath = path.resolve(dir, filename) + if (fs.lstatSync(fullpath).isDirectory()) { + postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback) + dirCallback(fullpath) + continue + } + fileCallback(fullpath) + } +} \ No newline at end of file diff --git a/packages/create-sonnet/utils/getCommand.js b/packages/create-sonnet/utils/getCommand.js new file mode 100644 index 0000000..856ed7a --- /dev/null +++ b/packages/create-sonnet/utils/getCommand.js @@ -0,0 +1,13 @@ +export default function getCommand(packageManager, scriptName, args) { + if (scriptName === 'install') { + return packageManager === 'yarn' ? 'yarn' : `${packageManager} install` + } + + if (args) { + return packageManager === 'npm' + ? `npm run ${scriptName} -- ${args}` + : `${packageManager} ${scriptName} ${args}` + } else { + return packageManager === 'npm' ? `npm run ${scriptName}` : `${packageManager} ${scriptName}` + } +} \ No newline at end of file diff --git a/packages/create-sonnet/utils/renderTemplate.js b/packages/create-sonnet/utils/renderTemplate.js new file mode 100644 index 0000000..5c3937c --- /dev/null +++ b/packages/create-sonnet/utils/renderTemplate.js @@ -0,0 +1,97 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import { pathToFileURL } from 'node:url' + +import deepMerge from './deepMerge.js' +import sortDependencies from './sortDependencies.js' + +/** + * Renders a template folder/file to the file system, + * by recursively copying all files under the `src` directory, + * with the following exception: + * - `_filename` should be renamed to `.filename` + * - Fields in `package.json` should be recursively merged + * @param {string} src source filename to copy + * @param {string} dest destination filename of the copy operation + */ +function renderTemplate(src, dest, callbacks) { + const stats = fs.statSync(src) + + if (stats.isDirectory()) { + // skip node_module + if (path.basename(src) === 'node_modules') { + return + } + + // if it's a directory, render its subdirectories and files recursively + fs.mkdirSync(dest, { recursive: true }) + for (const file of fs.readdirSync(src)) { + renderTemplate(path.resolve(src, file), path.resolve(dest, file), callbacks) + } + return + } + + const filename = path.basename(src) + + if (filename === 'package.json' && fs.existsSync(dest)) { + // merge instead of overwriting + const existing = JSON.parse(fs.readFileSync(dest, 'utf8')) + const newPackage = JSON.parse(fs.readFileSync(src, 'utf8')) + const pkg = sortDependencies(deepMerge(existing, newPackage)) + fs.writeFileSync(dest, JSON.stringify(pkg, null, 2) + '\n') + return + } + + if (filename === 'extensions.json' && fs.existsSync(dest)) { + // merge instead of overwriting + const existing = JSON.parse(fs.readFileSync(dest, 'utf8')) + const newExtensions = JSON.parse(fs.readFileSync(src, 'utf8')) + const extensions = deepMerge(existing, newExtensions) + fs.writeFileSync(dest, JSON.stringify(extensions, null, 2) + '\n') + return + } + + if (filename === 'settings.json' && fs.existsSync(dest)) { + // merge instead of overwriting + const existing = JSON.parse(fs.readFileSync(dest, 'utf8')) + const newSettings = JSON.parse(fs.readFileSync(src, 'utf8')) + const settings = deepMerge(existing, newSettings) + fs.writeFileSync(dest, JSON.stringify(settings, null, 2) + '\n') + return + } + + if (filename.startsWith('_')) { + // rename `_file` to `.file` + dest = path.resolve(path.dirname(dest), filename.replace(/^_/, '.')) + } + + if (filename === '_gitignore' && fs.existsSync(dest)) { + // append to existing .gitignore + const existing = fs.readFileSync(dest, 'utf8') + const newGitignore = fs.readFileSync(src, 'utf8') + fs.writeFileSync(dest, existing + '\n' + newGitignore) + return + } + + // data file for EJS templates + if (filename.endsWith('.data.mjs')) { + // use dest path as key for the data store + dest = dest.replace(/\.data\.mjs$/, '') + + // Add a callback to the array for late usage when template files are being processed + callbacks.push(async (dataStore) => { + const getData = (await import(pathToFileURL(src).toString())).default + + // Though current `getData` are all sync, we still retain the possibility of async + dataStore[dest] = await getData({ + oldData: dataStore[dest] || {} + }) + }) + + return // skip copying the data file + } + + fs.copyFileSync(src, dest) +} + +export default renderTemplate \ No newline at end of file diff --git a/packages/create-sonnet/utils/sortDependencies.js b/packages/create-sonnet/utils/sortDependencies.js new file mode 100644 index 0000000..5343ec2 --- /dev/null +++ b/packages/create-sonnet/utils/sortDependencies.js @@ -0,0 +1,22 @@ +export default function sortDependencies(packageJson) { + const sorted = {} + + const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] + + for (const depType of depTypes) { + if (packageJson[depType]) { + sorted[depType] = {} + + Object.keys(packageJson[depType]) + .sort() + .forEach((name) => { + sorted[depType][name] = packageJson[depType][name] + }) + } + } + + return { + ...packageJson, + ...sorted + } +} \ No newline at end of file diff --git a/packages/sonnet-core/package.json b/packages/sonnet-core/package.json index 9bb8777..847fc45 100644 --- a/packages/sonnet-core/package.json +++ b/packages/sonnet-core/package.json @@ -1,6 +1,6 @@ { "name": "@sonnetjs/core", - "version": "0.0.27", + "version": "0.0.28", "files": [ "dist" ], diff --git a/packages/sonnet-shared/package.json b/packages/sonnet-shared/package.json index ba9347d..6cee1ff 100644 --- a/packages/sonnet-shared/package.json +++ b/packages/sonnet-shared/package.json @@ -1,6 +1,6 @@ { "name": "@sonnetjs/shared", - "version": "0.0.3", + "version": "0.0.4", "files": [ "dist" ], diff --git a/packages/sonnet-store/package.json b/packages/sonnet-store/package.json index d3d7d60..a28aef6 100644 --- a/packages/sonnet-store/package.json +++ b/packages/sonnet-store/package.json @@ -1,6 +1,6 @@ { "name": "@sonnetjs/store", - "version": "0.0.2", + "version": "0.0.3", "files": [ "dist" ],