create-textmode 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/index.js +8 -0
- package/package.json +54 -0
- package/src/args.js +30 -0
- package/src/banner.js +57 -0
- package/src/cli.js +154 -0
- package/src/constants.js +19 -0
- package/src/fs-utils.js +85 -0
- package/src/packageManager.js +18 -0
- package/src/prompts.js +90 -0
- package/src/runCommand.js +46 -0
- package/src/summary.js +33 -0
- package/src/textmodeVersion.js +45 -0
- package/src/usage.js +18 -0
- package/src/versions.js +52 -0
- package/templates/vanilla-js/.eslintrc.json +13 -0
- package/templates/vanilla-js/.prettierrc +8 -0
- package/templates/vanilla-js/_gitignore +2 -0
- package/templates/vanilla-js/index.html +27 -0
- package/templates/vanilla-js/package.json +24 -0
- package/templates/vanilla-js/src/sketch.js +28 -0
- package/templates/vanilla-js/vite.config.js +7 -0
- package/templates/vanilla-ts/.eslintrc.json +19 -0
- package/templates/vanilla-ts/.prettierrc +8 -0
- package/templates/vanilla-ts/_gitignore +2 -0
- package/templates/vanilla-ts/index.html +28 -0
- package/templates/vanilla-ts/package.json +29 -0
- package/templates/vanilla-ts/src/sketch.ts +28 -0
- package/templates/vanilla-ts/tsconfig.json +15 -0
- package/templates/vanilla-ts/vite.config.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 humanbydefinition
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# create-textmode.js (✿◠‿◠)
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
A zero-friction scaffolder for `textmode.js`.
|
|
6
|
+
|
|
7
|
+
| [](https://developer.mozilla.org/docs/Web/JavaScript) [](https://vitejs.dev/) | [](https://github.com/humanbydefinition/textmode.js) [](https://code.textmode.art/) [](https://discord.gg/sjrw8QXNks) | [](https://ko-fi.com/V7V8JG2FY) [](https://github.com/sponsors/humanbydefinition) |
|
|
8
|
+
|:-------------|:-------------|:-------------|
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
`create-textmode.js` is a command-line tool to quickly scaffold new projects using `textmode.js` with sensible defaults and minimal friction.
|
|
13
|
+
|
|
14
|
+
Just run the CLI via `npm create textmode@latest` or your package manager's equivalent command, answer a few prompts, and you're ready to start building!
|
|
15
|
+
|
|
16
|
+
> [!NOTE]
|
|
17
|
+
> The CLI is still fresh out of the oven, so please report any issues or feature requests on the GitHub repo!
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js 18+ *(per `engines`)*
|
|
22
|
+
- One of: npm, pnpm, yarn, or bun on your PATH
|
|
23
|
+
|
|
24
|
+
## Quick start *(interactive)*
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm create textmode@latest
|
|
28
|
+
# or
|
|
29
|
+
pnpm create textmode@latest
|
|
30
|
+
# or
|
|
31
|
+
yarn create textmode
|
|
32
|
+
# ...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The CLI will prompt for project name, template, textmode.js version, installing dependencies, and starting the dev server.
|
|
36
|
+
|
|
37
|
+
## Non-interactive examples
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# TypeScript template, pin textmode.js, auto-install, do not run dev server
|
|
41
|
+
npm create textmode@latest my-textmode-app -- --template vanilla-ts --textmode-version 0.7.1 --install --no-run
|
|
42
|
+
|
|
43
|
+
# JavaScript template with pnpm, skip install and run
|
|
44
|
+
pnpm create textmode@latest demo -- --template vanilla-js --pm pnpm --no-install --no-run
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
- `--template <name>`: choose a template *(prompts if omitted)*
|
|
50
|
+
- `--name <projectName>` or first positional arg: directory/package name *(default suggestion if omitted)*
|
|
51
|
+
- `--textmode-version <ver>`: pin textmode.js *(prompts from fetched stable versions; defaults to `latest`)*
|
|
52
|
+
- `--pm <npm|pnpm|yarn|bun>`: force a package manager *(auto-detected otherwise)*
|
|
53
|
+
- `--install` / `--no-install`: install dependencies after scaffold *(prompts if neither is provided)*
|
|
54
|
+
- `--run` / `--no-run`: start the dev server after install *(prompts if neither is provided)*
|
|
55
|
+
- `--force`: allow using a non-empty directory without prompting
|
|
56
|
+
- `--help`: show usage
|
|
57
|
+
- `--version`: show the CLI version
|
|
58
|
+
|
|
59
|
+
## Templates
|
|
60
|
+
|
|
61
|
+
| Template | Stack | Tooling |
|
|
62
|
+
|:---------|:------|:--------|
|
|
63
|
+
| `vanilla-js` | Vite + JavaScript + `textmode.js` | ESLint *(JS rules)*, Prettier, `.gitignore` |
|
|
64
|
+
| `vanilla-ts` | Vite + TypeScript + `textmode.js` | `tsconfig`, ESLint *(@typescript-eslint)*, Prettier, `.gitignore`, `typecheck` |
|
|
65
|
+
|
|
66
|
+
Adding new templates? Follow the same table format so details stay consistent and easy to scan.
|
|
67
|
+
|
|
68
|
+
## Local development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install
|
|
72
|
+
npm test # vitest
|
|
73
|
+
npm run test:verbose
|
|
74
|
+
|
|
75
|
+
# Run CLI directly (no npm-create shim); no "--" needed
|
|
76
|
+
node bin/index.js demo --template vanilla-js --pm pnpm --no-install --no-run
|
|
77
|
+
# or
|
|
78
|
+
node bin/index.js
|
|
79
|
+
# ...
|
|
80
|
+
```
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-textmode",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Scaffold textmode.js projects with zero friction",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-textmode": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"textmode",
|
|
21
|
+
"textmode.js",
|
|
22
|
+
"create",
|
|
23
|
+
"scaffold",
|
|
24
|
+
"template"
|
|
25
|
+
],
|
|
26
|
+
"author": "humanbydefinition",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"homepage": "https://github.com/humanbydefinition/create-textmode.js#readme",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/humanbydefinition/create-textmode.js.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/humanbydefinition/create-textmode.js/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "vitest",
|
|
41
|
+
"test:verbose": "vitest run --reporter=verbose"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@clack/prompts": "^0.11.0",
|
|
45
|
+
"boxen": "^7.1.1",
|
|
46
|
+
"kleur": "^4.1.5",
|
|
47
|
+
"minimist": "^1.2.8",
|
|
48
|
+
"unique-names-generator": "^4.7.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"textmode.js": "^0.7.1",
|
|
52
|
+
"vitest": "^4.0.15"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import minimist from 'minimist';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { templates } from './constants.js';
|
|
4
|
+
import { printUsage } from './usage.js';
|
|
5
|
+
|
|
6
|
+
export function parseArgv(rawArgs) {
|
|
7
|
+
return minimist(rawArgs, {
|
|
8
|
+
alias: {
|
|
9
|
+
h: 'help',
|
|
10
|
+
t: 'template',
|
|
11
|
+
n: 'name',
|
|
12
|
+
f: 'force',
|
|
13
|
+
v: 'version',
|
|
14
|
+
tv: 'textmode-version'
|
|
15
|
+
},
|
|
16
|
+
string: ['template', 'name', 'pm', 'textmode-version'],
|
|
17
|
+
boolean: ['help', 'force', 'version', 'install', 'run', 'no-install', 'no-run'],
|
|
18
|
+
default: { install: null, run: null }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ensureKnownTemplate(name) {
|
|
23
|
+
if (!name) return;
|
|
24
|
+
const found = templates.find((t) => t.name === name);
|
|
25
|
+
if (found) return;
|
|
26
|
+
|
|
27
|
+
console.error(kleur.red(`Unknown template: ${name}`));
|
|
28
|
+
printUsage();
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { HEADER } from './constants.js';
|
|
3
|
+
|
|
4
|
+
const SPLIT_FLAP_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@%&/\\<>[]=+*';
|
|
5
|
+
|
|
6
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
|
|
8
|
+
function randomFlapChar() {
|
|
9
|
+
const idx = Math.floor(Math.random() * SPLIT_FLAP_CHARS.length);
|
|
10
|
+
return SPLIT_FLAP_CHARS[idx];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function animateSplitFlap(lines, { flips = 16, frameDelay = 32, columnStagger = 6 } = {}) {
|
|
14
|
+
const canAnimate = process.stdout.isTTY && process.env.CI !== 'true';
|
|
15
|
+
if (!canAnimate) {
|
|
16
|
+
console.log(kleur.cyan(lines.join('\n')));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lineCount = lines.length;
|
|
21
|
+
const blankFrame = lines.map((line) => line.replace(/[^\s]/g, ' '));
|
|
22
|
+
let firstRender = true;
|
|
23
|
+
|
|
24
|
+
const render = (frame) => {
|
|
25
|
+
if (!firstRender) {
|
|
26
|
+
process.stdout.write(`\x1b[${lineCount}F`);
|
|
27
|
+
} else {
|
|
28
|
+
firstRender = false;
|
|
29
|
+
}
|
|
30
|
+
process.stdout.write(frame.join('\n'));
|
|
31
|
+
process.stdout.write('\n');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
render(blankFrame);
|
|
35
|
+
|
|
36
|
+
for (let flip = 0; flip < flips; flip++) {
|
|
37
|
+
const frameLines = lines.map((line) => {
|
|
38
|
+
const chars = line.split('').map((ch, idx) => {
|
|
39
|
+
if (ch === ' ') return ' ';
|
|
40
|
+
const shouldSettle = flip >= flips - 2 || flip > Math.floor(idx / columnStagger);
|
|
41
|
+
return shouldSettle ? ch : randomFlapChar();
|
|
42
|
+
});
|
|
43
|
+
return kleur.cyan(chars.join(''));
|
|
44
|
+
});
|
|
45
|
+
render(frameLines);
|
|
46
|
+
await sleep(frameDelay + flip * 6);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render(lines.map((line) => kleur.cyan(line)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function printHeader() {
|
|
53
|
+
const headerLines = HEADER.trim().split('\n');
|
|
54
|
+
console.log('');
|
|
55
|
+
await animateSplitFlap(headerLines);
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import kleur from 'kleur';
|
|
5
|
+
import { intro, outro, spinner, log } from '@clack/prompts';
|
|
6
|
+
import { detectPackageManager, pmCommands } from './packageManager.js';
|
|
7
|
+
import { isEmptyDir, pathExists, scaffoldTemplate } from './fs-utils.js';
|
|
8
|
+
import { runCommand, runCommandLogged } from './runCommand.js';
|
|
9
|
+
import { printUsage } from './usage.js';
|
|
10
|
+
import { templates } from './constants.js';
|
|
11
|
+
import {
|
|
12
|
+
promptInstall,
|
|
13
|
+
promptProjectName,
|
|
14
|
+
promptRun,
|
|
15
|
+
promptTemplate,
|
|
16
|
+
promptOverwrite,
|
|
17
|
+
suggestProjectName,
|
|
18
|
+
promptTextmodeVersion
|
|
19
|
+
} from './prompts.js';
|
|
20
|
+
import { printHeader } from './banner.js';
|
|
21
|
+
import { parseArgv, ensureKnownTemplate } from './args.js';
|
|
22
|
+
import { resolveTextmodeVersion } from './textmodeVersion.js';
|
|
23
|
+
import { printSummary } from './summary.js';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
|
|
28
|
+
export async function run() {
|
|
29
|
+
await printHeader();
|
|
30
|
+
intro(
|
|
31
|
+
kleur.cyan('Welcome to textmode.js - let’s scaffold a new project! ✿ ') +
|
|
32
|
+
kleur.gray('(Press Ctrl+C at any time to cancel.)')
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const argv = parseArgv(process.argv.slice(2));
|
|
36
|
+
|
|
37
|
+
if (argv.help) {
|
|
38
|
+
printUsage();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (argv.version) {
|
|
43
|
+
const pkgJson = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
44
|
+
console.log(pkgJson.version);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Gather project info ---
|
|
49
|
+
let projectName = argv._[0] || argv.name;
|
|
50
|
+
if (!projectName) {
|
|
51
|
+
const suggested = suggestProjectName();
|
|
52
|
+
projectName = await promptProjectName(suggested);
|
|
53
|
+
}
|
|
54
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
55
|
+
|
|
56
|
+
ensureKnownTemplate(argv.template);
|
|
57
|
+
let templateName = argv.template;
|
|
58
|
+
if (!templateName) {
|
|
59
|
+
templateName = await promptTemplate();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const template = templates.find((t) => t.name === templateName);
|
|
63
|
+
const templateDir = path.join(__dirname, '..', 'templates', template.dir);
|
|
64
|
+
|
|
65
|
+
// --- Resolve textmode.js version ---
|
|
66
|
+
const requestedTextmodeVersion = argv['textmode-version'];
|
|
67
|
+
const { textmodeVersion } = await resolveTextmodeVersion(
|
|
68
|
+
requestedTextmodeVersion,
|
|
69
|
+
promptTextmodeVersion
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// --- Pre-scaffold checks ---
|
|
73
|
+
const pm = argv.pm || detectPackageManager();
|
|
74
|
+
const pmCmds = pmCommands(pm);
|
|
75
|
+
|
|
76
|
+
const targetExists = await pathExists(targetDir);
|
|
77
|
+
if (targetExists && !(await isEmptyDir(targetDir)) && !argv.force) {
|
|
78
|
+
const overwrite = await promptOverwrite(targetDir);
|
|
79
|
+
if (!overwrite) {
|
|
80
|
+
outro('Aborted.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Scaffold project ---
|
|
86
|
+
const scaffoldSpin = spinner();
|
|
87
|
+
scaffoldSpin.start('Scaffolding project...');
|
|
88
|
+
await scaffoldTemplate({ templateDir, targetDir, projectName, textmodeVersion });
|
|
89
|
+
scaffoldSpin.stop('Scaffold complete.');
|
|
90
|
+
|
|
91
|
+
// --- Install dependencies ---
|
|
92
|
+
let installDone = false;
|
|
93
|
+
|
|
94
|
+
const userInstall = argv.install;
|
|
95
|
+
const userNoInstall = argv['no-install'];
|
|
96
|
+
const hasInstallFlag = userInstall !== null || userNoInstall === true;
|
|
97
|
+
let doInstall = userInstall === true;
|
|
98
|
+
|
|
99
|
+
if (!hasInstallFlag) {
|
|
100
|
+
doInstall = await promptInstall(pm);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (doInstall) {
|
|
104
|
+
const installSpin = spinner();
|
|
105
|
+
installSpin.start(`Installing dependencies with ${pm}...`);
|
|
106
|
+
let seenOutput = false;
|
|
107
|
+
try {
|
|
108
|
+
await runCommandLogged(pm, pmCmds.install, targetDir, (line) => {
|
|
109
|
+
if (!seenOutput) {
|
|
110
|
+
installSpin.stop(`Installing dependencies with ${pm}...`);
|
|
111
|
+
seenOutput = true;
|
|
112
|
+
}
|
|
113
|
+
console.log(`│ ${line}`);
|
|
114
|
+
});
|
|
115
|
+
installDone = true;
|
|
116
|
+
if (!seenOutput) installSpin.stop('Dependencies installed.');
|
|
117
|
+
log.success('Dependencies installed.');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
installSpin.stop('Dependency installation failed.');
|
|
120
|
+
log.error(`Dependency installation failed: ${err.message || err}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Run dev server ---
|
|
125
|
+
let runDone = false;
|
|
126
|
+
|
|
127
|
+
if (installDone) {
|
|
128
|
+
const userRun = argv.run;
|
|
129
|
+
const userNoRun = argv['no-run'];
|
|
130
|
+
const hasRunFlag = userRun !== null || userNoRun === true;
|
|
131
|
+
let doRun = userRun === true;
|
|
132
|
+
|
|
133
|
+
if (!hasRunFlag) {
|
|
134
|
+
doRun = await promptRun(pm);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (doRun) {
|
|
138
|
+
log.step('Starting dev server...');
|
|
139
|
+
try {
|
|
140
|
+
await runCommand(pm, pmCmds.runDev, targetDir);
|
|
141
|
+
runDone = true;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log.error(err.message || String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Summary ---
|
|
149
|
+
log.message('');
|
|
150
|
+
printSummary({ projectName, pm, pmCmds, installDone, runDone });
|
|
151
|
+
outro(kleur.green('Enjoy textmode.js! ツ'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default run;
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const HEADER = `
|
|
2
|
+
████████╗███████╗██╗ ██╗████████╗███╗ ███╗ ██████╗ ██████╗ ███████╗ ██╗███████╗
|
|
3
|
+
╚══██╔══╝██╔════╝╚██╗██╔╝╚══██╔══╝████╗ ████║██╔═══██╗██╔══██╗██╔════╝ ██║██╔════╝
|
|
4
|
+
██║ █████╗ ╚███╔╝ ██║ ██╔████╔██║██║ ██║██║ ██║█████╗ ██║███████╗
|
|
5
|
+
██║ ██╔══╝ ██╔██╗ ██║ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██ ██║╚════██║
|
|
6
|
+
██║ ███████╗██╔╝ ██╗ ██║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗██╗╚█████╔╝███████║
|
|
7
|
+
╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚════╝ ╚══════╝
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export const templates = [
|
|
11
|
+
{ name: 'vanilla-js', label: 'Vanilla JavaScript (vite)', dir: 'vanilla-js' },
|
|
12
|
+
{ name: 'vanilla-ts', label: 'Vanilla TypeScript (vite)', dir: 'vanilla-ts' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// File name patterns considered text for placeholder replacement.
|
|
16
|
+
export const TEXT_FILE_REGEX = /\.(json|js|jsx|ts|tsx|vue|md|html|txt|cjs|mjs)$/i;
|
|
17
|
+
|
|
18
|
+
// Entries ignored when deciding if a directory is empty enough to reuse.
|
|
19
|
+
export const IGNORED_DIR_ENTRIES = ['.git', '.gitkeep'];
|
package/src/fs-utils.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rename, stat, writeFile } from 'fs/promises';
|
|
3
|
+
import { IGNORED_DIR_ENTRIES, TEXT_FILE_REGEX } from './constants.js';
|
|
4
|
+
|
|
5
|
+
export async function pathExists(p) {
|
|
6
|
+
try {
|
|
7
|
+
await stat(p);
|
|
8
|
+
return true;
|
|
9
|
+
} catch (err) {
|
|
10
|
+
if (err.code === 'ENOENT') return false;
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function isEmptyDir(dir) {
|
|
16
|
+
try {
|
|
17
|
+
const entries = await readdir(dir);
|
|
18
|
+
const filtered = entries.filter((name) => !IGNORED_DIR_ENTRIES.includes(name));
|
|
19
|
+
return filtered.length === 0;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === 'ENOENT') return true;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function replacePlaceholders(root, projectName) {
|
|
27
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(root, entry.name);
|
|
31
|
+
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
await replacePlaceholders(fullPath, projectName);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (entry.name === '_gitignore') {
|
|
38
|
+
await rename(fullPath, path.join(root, '.gitignore'));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!TEXT_FILE_REGEX.test(entry.name)) continue;
|
|
43
|
+
|
|
44
|
+
const data = await readFile(fullPath, 'utf8');
|
|
45
|
+
if (!data.includes('{{name}}')) continue;
|
|
46
|
+
|
|
47
|
+
const next = data.replace(/{{name}}/g, projectName);
|
|
48
|
+
await writeFile(fullPath, next, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function setPackageName(targetDir, projectName) {
|
|
53
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
56
|
+
pkg.name = projectName;
|
|
57
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// If no package.json exists, ignore; templates without package.json are allowed.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function setTextmodeVersion(targetDir, textmodeVersion) {
|
|
64
|
+
if (!textmodeVersion) return;
|
|
65
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
68
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
69
|
+
if (pkg.dependencies['textmode.js'] !== undefined) {
|
|
70
|
+
pkg.dependencies['textmode.js'] = textmodeVersion;
|
|
71
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Ignore templates without package.json or invalid JSON.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function scaffoldTemplate({ templateDir, targetDir, projectName, textmodeVersion }) {
|
|
79
|
+
await mkdir(targetDir, { recursive: true });
|
|
80
|
+
await cp(templateDir, targetDir, { recursive: true, force: true });
|
|
81
|
+
|
|
82
|
+
await replacePlaceholders(targetDir, projectName);
|
|
83
|
+
await setPackageName(targetDir, projectName);
|
|
84
|
+
await setTextmodeVersion(targetDir, textmodeVersion);
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function detectPackageManager() {
|
|
2
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
3
|
+
if (ua.includes('pnpm')) return 'pnpm';
|
|
4
|
+
if (ua.includes('yarn')) return 'yarn';
|
|
5
|
+
if (ua.includes('bun')) return 'bun';
|
|
6
|
+
return 'npm';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function pmCommands(pm) {
|
|
10
|
+
switch (pm) {
|
|
11
|
+
case 'pnpm':
|
|
12
|
+
case 'yarn':
|
|
13
|
+
case 'bun':
|
|
14
|
+
return { install: ['install'], runDev: ['run', 'dev'] };
|
|
15
|
+
default:
|
|
16
|
+
return { install: ['install'], runDev: ['run', 'dev'] };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { confirm, isCancel, cancel, select, text } from '@clack/prompts';
|
|
3
|
+
import kleur from 'kleur';
|
|
4
|
+
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
|
5
|
+
import { templates } from './constants.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle user cancellation uniformly.
|
|
9
|
+
* @param {unknown} value - The value returned from a prompt.
|
|
10
|
+
* @returns {boolean} True if the user cancelled.
|
|
11
|
+
*/
|
|
12
|
+
export function handleCancel(value) {
|
|
13
|
+
if (isCancel(value)) {
|
|
14
|
+
cancel('Operation cancelled.');
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function promptTemplate() {
|
|
21
|
+
const choice = await select({
|
|
22
|
+
message: `${kleur.cyan('Select a template')} ${kleur.gray('(↑↓ move, ↵ confirm)')}`,
|
|
23
|
+
options: templates.map((t) => ({ value: t.name, label: t.label })),
|
|
24
|
+
initialValue: templates[0].name
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
handleCancel(choice);
|
|
28
|
+
return choice;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function suggestProjectName() {
|
|
32
|
+
return uniqueNamesGenerator({
|
|
33
|
+
dictionaries: [adjectives, colors, animals],
|
|
34
|
+
separator: '-',
|
|
35
|
+
length: 3
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function promptProjectName(defaultName) {
|
|
40
|
+
const name = await text({
|
|
41
|
+
message: `${kleur.cyan('Project name')} ${kleur.gray('(enter to accept default)')}`,
|
|
42
|
+
initialValue: defaultName,
|
|
43
|
+
validate: (value) => (value && value.trim().length > 0 ? undefined : 'Name cannot be empty')
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
handleCancel(name);
|
|
47
|
+
return name.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function promptOverwrite(targetDir) {
|
|
51
|
+
const response = await confirm({
|
|
52
|
+
message: `Directory ${path.basename(targetDir)} is not empty. Continue?`,
|
|
53
|
+
initialValue: false
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
handleCancel(response);
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function promptInstall(pm) {
|
|
61
|
+
const decision = await confirm({
|
|
62
|
+
message: `Install dependencies with ${pm}?`,
|
|
63
|
+
initialValue: true
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
handleCancel(decision);
|
|
67
|
+
return decision;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function promptRun(pm) {
|
|
71
|
+
const decision = await confirm({
|
|
72
|
+
message: `Run dev server now with ${pm}?`,
|
|
73
|
+
initialValue: false
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
handleCancel(decision);
|
|
77
|
+
return decision;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function promptTextmodeVersion(options) {
|
|
81
|
+
const choice = await select({
|
|
82
|
+
message: `${kleur.cyan('Select textmode.js version')} ${kleur.gray('(latest recommended)')}`,
|
|
83
|
+
options,
|
|
84
|
+
initialValue: options[0]?.value,
|
|
85
|
+
maxItems: 5
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
handleCancel(choice);
|
|
89
|
+
return choice;
|
|
90
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
|
|
4
|
+
export function runCommand(cmd, args, cwd) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const child = spawn(cmd, args, {
|
|
7
|
+
cwd,
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
shell: process.platform === 'win32'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
child.on('exit', (code) => {
|
|
13
|
+
if (code === 0) resolve();
|
|
14
|
+
else reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on('error', reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runCommandLogged(cmd, args, cwd, onLine) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = spawn(cmd, args, {
|
|
24
|
+
cwd,
|
|
25
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
26
|
+
shell: process.platform === 'win32'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const forward = (stream) => {
|
|
30
|
+
if (!stream) return;
|
|
31
|
+
const rl = readline.createInterface({ input: stream });
|
|
32
|
+
rl.on('line', (line) => onLine(line));
|
|
33
|
+
rl.on('error', () => {});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
forward(child.stdout);
|
|
37
|
+
forward(child.stderr);
|
|
38
|
+
|
|
39
|
+
child.on('exit', (code) => {
|
|
40
|
+
if (code === 0) resolve();
|
|
41
|
+
else reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.on('error', reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
package/src/summary.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
|
|
4
|
+
export function printSummary({ projectName, pm, pmCmds, installDone, runDone }) {
|
|
5
|
+
const installCmd = `${pm} ${pmCmds.install.join(' ')}`;
|
|
6
|
+
const runCmd = `${pm} ${pmCmds.runDev.join(' ')}`;
|
|
7
|
+
|
|
8
|
+
const steps = [
|
|
9
|
+
`cd ${projectName}`,
|
|
10
|
+
installDone ? `✓ already ran ${installCmd}` : installCmd,
|
|
11
|
+
runDone ? `✓ dev server is running (${runCmd})` : runCmd
|
|
12
|
+
]
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.join('\n');
|
|
15
|
+
|
|
16
|
+
const infoLines = [
|
|
17
|
+
'',
|
|
18
|
+
'',
|
|
19
|
+
kleur.bold().cyan('Helpful links:'),
|
|
20
|
+
`${kleur.cyan(' Documentation:')} https://code.textmode.art`,
|
|
21
|
+
`${kleur.cyan(' Community:')} https://discord.gg/sjrw8QXNks`,
|
|
22
|
+
`${kleur.cyan(' CLI issues:')} https://github.com/humanbydefinition/create-textmode.js/issues`
|
|
23
|
+
].join('\n');
|
|
24
|
+
|
|
25
|
+
const boxed = boxen(`${kleur.bold().cyan('Next steps:')}\n${steps}${infoLines}`, {
|
|
26
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
27
|
+
margin: { top: 0, bottom: 0 },
|
|
28
|
+
borderStyle: 'round',
|
|
29
|
+
borderColor: 'cyan'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(boxed);
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spinner, log } from '@clack/prompts';
|
|
2
|
+
import { getTextmodeVersions } from './versions.js';
|
|
3
|
+
|
|
4
|
+
export async function resolveTextmodeVersion(requestedTextmodeVersion, promptTextmodeVersion) {
|
|
5
|
+
let textmodeVersion = 'latest';
|
|
6
|
+
let stableVersions = [];
|
|
7
|
+
|
|
8
|
+
const versionSpinner = spinner();
|
|
9
|
+
versionSpinner.start('Fetching textmode.js versions...');
|
|
10
|
+
try {
|
|
11
|
+
stableVersions = await getTextmodeVersions();
|
|
12
|
+
if (stableVersions.length === 0) throw new Error('No versions found');
|
|
13
|
+
versionSpinner.stop('Fetched textmode.js versions.');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
versionSpinner.stop('Could not fetch versions.');
|
|
16
|
+
log.warn('Using latest version as fallback.');
|
|
17
|
+
stableVersions = [];
|
|
18
|
+
textmodeVersion = 'latest';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const latestVersion = stableVersions[0];
|
|
22
|
+
const availableOptions = [
|
|
23
|
+
{
|
|
24
|
+
value: 'latest',
|
|
25
|
+
label: latestVersion ? `latest (${latestVersion})` : 'latest (recommended)'
|
|
26
|
+
},
|
|
27
|
+
...stableVersions.slice(1).map((v) => ({ value: v, label: v }))
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (requestedTextmodeVersion) {
|
|
31
|
+
const found = availableOptions.find((opt) => opt.value === requestedTextmodeVersion);
|
|
32
|
+
if (found) {
|
|
33
|
+
textmodeVersion = requestedTextmodeVersion;
|
|
34
|
+
} else if (stableVersions.includes(requestedTextmodeVersion)) {
|
|
35
|
+
textmodeVersion = requestedTextmodeVersion;
|
|
36
|
+
} else {
|
|
37
|
+
log.warn(`Requested textmode.js@${requestedTextmodeVersion} not found; using latest instead.`);
|
|
38
|
+
textmodeVersion = 'latest';
|
|
39
|
+
}
|
|
40
|
+
} else if (stableVersions.length > 0) {
|
|
41
|
+
textmodeVersion = await promptTextmodeVersion(availableOptions);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { textmodeVersion, stableVersions, availableOptions };
|
|
45
|
+
}
|
package/src/usage.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { templates } from './constants.js';
|
|
2
|
+
|
|
3
|
+
export function printUsage() {
|
|
4
|
+
const list = templates.map((t) => ` - ${t.name}`).join('\n');
|
|
5
|
+
|
|
6
|
+
console.log(`Usage: npm create textmode@latest [project-name] -- [options]\n`);
|
|
7
|
+
console.log('Options:');
|
|
8
|
+
console.log(' --template <name> Choose a template');
|
|
9
|
+
console.log(' --name <name> Project directory name (alias: positional arg)');
|
|
10
|
+
console.log(' --pm <npm|pnpm|yarn|bun> Force package manager (auto-detected if omitted)');
|
|
11
|
+
console.log(' --textmode-version <ver> Pin textmode.js version (default: latest, prompts if omitted)');
|
|
12
|
+
console.log(' --install / --no-install Install dependencies after scaffold');
|
|
13
|
+
console.log(' --run / --no-run Run dev server after install');
|
|
14
|
+
console.log(' --force Allow using a non-empty directory');
|
|
15
|
+
console.log(' --help Show this help');
|
|
16
|
+
console.log(' --version Show CLI version');
|
|
17
|
+
console.log('\nTemplates:\n' + list);
|
|
18
|
+
}
|
package/src/versions.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
|
|
3
|
+
let cachedVersions = null;
|
|
4
|
+
|
|
5
|
+
function fetchJson(url) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
https
|
|
8
|
+
.get(url, (res) => {
|
|
9
|
+
let data = '';
|
|
10
|
+
res.on('data', (chunk) => {
|
|
11
|
+
data += chunk;
|
|
12
|
+
});
|
|
13
|
+
res.on('end', () => {
|
|
14
|
+
try {
|
|
15
|
+
resolve(JSON.parse(data));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
reject(err);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
})
|
|
21
|
+
.on('error', reject);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isStable(version) {
|
|
26
|
+
// Exclude prerelease tags like -beta, -rc, etc.
|
|
27
|
+
return !version.includes('-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function compareSemverDesc(a, b) {
|
|
31
|
+
const pa = a.split('.').map(Number);
|
|
32
|
+
const pb = b.split('.').map(Number);
|
|
33
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
|
|
34
|
+
const va = pa[i] || 0;
|
|
35
|
+
const vb = pb[i] || 0;
|
|
36
|
+
if (va > vb) return -1;
|
|
37
|
+
if (va < vb) return 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function getTextmodeVersions(limit = 20) {
|
|
43
|
+
if (cachedVersions) return cachedVersions.slice(0, limit);
|
|
44
|
+
|
|
45
|
+
const data = await fetchJson('https://registry.npmjs.org/textmode.js');
|
|
46
|
+
const versions = Object.keys(data.versions || {})
|
|
47
|
+
.filter(isStable)
|
|
48
|
+
.sort(compareSemverDesc);
|
|
49
|
+
|
|
50
|
+
cachedVersions = versions;
|
|
51
|
+
return versions.slice(0, limit);
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{name}}</title>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
canvas {
|
|
17
|
+
display: block;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
|
|
22
|
+
<body>
|
|
23
|
+
<div id="app"></div>
|
|
24
|
+
<script type="module" src="/src/sketch.js"></script>
|
|
25
|
+
</body>
|
|
26
|
+
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"format": "prettier --write \"src/**/*.{js,ts}\"",
|
|
11
|
+
"format:check": "prettier --check \"src/**/*.{js,ts}\"",
|
|
12
|
+
"lint": "eslint src --ext .js",
|
|
13
|
+
"lint:fix": "eslint src --ext .js --fix"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"textmode.js": "latest"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"prettier": "^3.7.4",
|
|
20
|
+
"vite": "^7.2.7",
|
|
21
|
+
"eslint": "^9.39.1",
|
|
22
|
+
"eslint-config-prettier": "^10.1.8"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { textmode } from 'textmode.js';
|
|
2
|
+
|
|
3
|
+
const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
|
|
4
|
+
|
|
5
|
+
tm.draw(() => {
|
|
6
|
+
tm.background(0);
|
|
7
|
+
|
|
8
|
+
const halfCols = (tm.grid.cols / 2) + 1;
|
|
9
|
+
const halfRows = (tm.grid.rows / 2) + 1;
|
|
10
|
+
|
|
11
|
+
for (let y = -halfRows; y < halfRows; y++) {
|
|
12
|
+
for (let x = -halfCols; x < halfCols; x++) {
|
|
13
|
+
const dist = Math.sqrt(x * x + y * y);
|
|
14
|
+
const wave = Math.sin(dist * 0.2 - tm.frameCount * 0.1);
|
|
15
|
+
|
|
16
|
+
tm.push();
|
|
17
|
+
tm.translate(x, y, 0);
|
|
18
|
+
tm.char(wave > 0.5 ? '▓' : wave > 0 ? '▒' : '░');
|
|
19
|
+
tm.charColor(0, 150 + wave * 100, 255);
|
|
20
|
+
tm.point();
|
|
21
|
+
tm.pop();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
tm.windowResized(() => {
|
|
27
|
+
tm.resizeCanvas(window.innerWidth, window.innerHeight);
|
|
28
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"env": {
|
|
4
|
+
"browser": true,
|
|
5
|
+
"es2021": true
|
|
6
|
+
},
|
|
7
|
+
"parser": "@typescript-eslint/parser",
|
|
8
|
+
"parserOptions": {
|
|
9
|
+
"ecmaVersion": "latest",
|
|
10
|
+
"sourceType": "module"
|
|
11
|
+
},
|
|
12
|
+
"plugins": ["@typescript-eslint"],
|
|
13
|
+
"extends": [
|
|
14
|
+
"eslint:recommended",
|
|
15
|
+
"plugin:@typescript-eslint/recommended",
|
|
16
|
+
"prettier"
|
|
17
|
+
],
|
|
18
|
+
"ignorePatterns": ["dist/", "node_modules/"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{name}}</title>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
canvas {
|
|
17
|
+
display: block;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<div id="app"></div>
|
|
25
|
+
<script type="module" src="/src/sketch.ts"></script>
|
|
26
|
+
</body>
|
|
27
|
+
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"format": "prettier --write \"src/**/*.{js,ts}\"",
|
|
11
|
+
"format:check": "prettier --check \"src/**/*.{js,ts}\"",
|
|
12
|
+
"lint": "eslint src --ext .ts",
|
|
13
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"textmode.js": "latest"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"prettier": "^3.7.4",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"vite": "^7.2.7",
|
|
23
|
+
"eslint": "^9.39.1",
|
|
24
|
+
"eslint-config-prettier": "^10.1.8",
|
|
25
|
+
"@types/node": "^24.10.2",
|
|
26
|
+
"@typescript-eslint/parser": "^8.49.0",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.49.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { textmode } from 'textmode.js';
|
|
2
|
+
|
|
3
|
+
const tm = textmode.create({ width: window.innerWidth, height: window.innerHeight });
|
|
4
|
+
|
|
5
|
+
tm.draw(() => {
|
|
6
|
+
tm.background(0);
|
|
7
|
+
|
|
8
|
+
const halfCols = tm.grid.cols / 2 + 1;
|
|
9
|
+
const halfRows = tm.grid.rows / 2 + 1;
|
|
10
|
+
|
|
11
|
+
for (let y = -halfRows; y < halfRows; y++) {
|
|
12
|
+
for (let x = -halfCols; x < halfCols; x++) {
|
|
13
|
+
const dist = Math.sqrt(x * x + y * y);
|
|
14
|
+
const wave = Math.sin(dist * 0.2 - tm.frameCount * 0.1);
|
|
15
|
+
|
|
16
|
+
tm.push();
|
|
17
|
+
tm.translate(x, y, 0);
|
|
18
|
+
tm.char(wave > 0.5 ? '▓' : wave > 0 ? '▒' : '░');
|
|
19
|
+
tm.charColor(0, 150 + wave * 100, 255);
|
|
20
|
+
tm.point();
|
|
21
|
+
tm.pop();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
tm.windowResized(() => {
|
|
27
|
+
tm.resizeCanvas(window.innerWidth, window.innerHeight);
|
|
28
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Node",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"jsx": "preserve",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["vite/client"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"],
|
|
14
|
+
"references": []
|
|
15
|
+
}
|