@phreshos/cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -19
- package/dist/build-template.js +73 -42
- package/dist/cli.js +133 -241
- package/dist/create.js +72 -63
- package/dist/derive.js +4 -4
- package/dist/init.js +113 -122
- package/dist/pack.js +9 -8
- package/dist/program-intake.js +10 -2
- package/dist/project-dependency.js +32 -14
- package/dist/project.js +2 -3
- package/dist/prompts.js +105 -0
- package/dist/style.js +9 -14
- package/dist/template/README.md +1 -1
- package/dist/template/icon.png +0 -0
- package/dist/template/package.json +8 -7
- package/dist/template/phresh.config.ts +65 -25
- package/dist/template/source/build.ts +3 -3
- package/dist/template/source/client/app.tsx +3 -8
- package/dist/template/source/server/main.ts +1 -1
- package/dist/template/vite.client.ts +22 -0
- package/dist/template/vite.server.ts +27 -0
- package/dist/template.json +5 -0
- package/package.json +8 -4
- package/dist/questions.js +0 -25
- package/dist/relative-value.js +0 -118
- package/dist/template/icons/128x128.png +0 -0
- package/dist/template/vite.config.ts +0 -38
package/dist/project.js
CHANGED
|
@@ -6,11 +6,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
import { layers } from "@phreshos/core";
|
|
9
|
+
import { isRelativeValue, layers } from "@phreshos/core";
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
|
-
import { isRelativeValue } from "./relative-value.js";
|
|
14
13
|
export const configFile = "phresh.config.ts";
|
|
15
14
|
export async function readConfig(directory = process.cwd()) {
|
|
16
15
|
const path = resolve(directory, configFile);
|
|
@@ -40,7 +39,7 @@ function coherent(config) {
|
|
|
40
39
|
throw new Error("A program's identity is kebab-case, because it is also the name of its directory");
|
|
41
40
|
if (!config.server && !config.client)
|
|
42
41
|
throw new Error("A program must have a server half, a client half, or both");
|
|
43
|
-
for (const field of ["name", "version", "description", "
|
|
42
|
+
for (const field of ["name", "version", "description", "icon"]) {
|
|
44
43
|
if (config[field] !== undefined && typeof config[field] !== "string")
|
|
45
44
|
throw new Error(`A program's ${field} must be text`);
|
|
46
45
|
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
2
|
+
import colors from "picocolors";
|
|
3
|
+
import { column, heading, line } from "./style.js";
|
|
4
|
+
/** Signals an ordinary interactive cancellation rather than an operation failure. */
|
|
5
|
+
export class PromptCancelled extends Error {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* One interaction language shared by commands that can ask questions.
|
|
9
|
+
*
|
|
10
|
+
* Clack owns terminal behavior. This adapter owns only the product's wording
|
|
11
|
+
* and the rule that automation never opens or waits for a prompt.
|
|
12
|
+
*/
|
|
13
|
+
export default function prompts() {
|
|
14
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
15
|
+
function begin(title, context) {
|
|
16
|
+
if (interactive)
|
|
17
|
+
intro(`${colors.bold(title)}${context ? ` ${colors.dim(`· ${context}`)}` : ""}`);
|
|
18
|
+
else
|
|
19
|
+
heading(title, context);
|
|
20
|
+
}
|
|
21
|
+
function finish(message) {
|
|
22
|
+
if (interactive)
|
|
23
|
+
outro(colors.bold(message));
|
|
24
|
+
else
|
|
25
|
+
heading(message);
|
|
26
|
+
}
|
|
27
|
+
function detail(label, value, source) {
|
|
28
|
+
if (interactive)
|
|
29
|
+
log.message(`${colors.dim(column(label))}${value}${source ? ` ${colors.dim(source)}` : ""}`);
|
|
30
|
+
else
|
|
31
|
+
line(label, value, source);
|
|
32
|
+
}
|
|
33
|
+
function message(value = "") {
|
|
34
|
+
if (interactive)
|
|
35
|
+
log.message(value);
|
|
36
|
+
else
|
|
37
|
+
console.log(value ? ` ${value}` : "");
|
|
38
|
+
}
|
|
39
|
+
async function progress(message, completed, work) {
|
|
40
|
+
if (!interactive)
|
|
41
|
+
return await work();
|
|
42
|
+
const indicator = spinner();
|
|
43
|
+
indicator.start(message);
|
|
44
|
+
try {
|
|
45
|
+
const result = await work();
|
|
46
|
+
indicator.clear();
|
|
47
|
+
log.success(completed, { spacing: 0 });
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
indicator.error(`${message} failed`);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function ask(explanation, question, fallback) {
|
|
56
|
+
if (!interactive)
|
|
57
|
+
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
58
|
+
log.message(colors.dim(explanation));
|
|
59
|
+
const value = await text({
|
|
60
|
+
message: question,
|
|
61
|
+
placeholder: fallback,
|
|
62
|
+
defaultValue: fallback
|
|
63
|
+
});
|
|
64
|
+
if (isCancel(value))
|
|
65
|
+
stop();
|
|
66
|
+
return value.trim();
|
|
67
|
+
}
|
|
68
|
+
async function yes(explanation, question, fallback) {
|
|
69
|
+
if (!interactive)
|
|
70
|
+
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
71
|
+
log.message(colors.dim(explanation));
|
|
72
|
+
const value = await confirm({ message: question, initialValue: fallback });
|
|
73
|
+
if (isCancel(value))
|
|
74
|
+
stop();
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
async function choose(explanation, question, values, fallback) {
|
|
78
|
+
if (!interactive)
|
|
79
|
+
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
80
|
+
log.message(colors.dim(explanation));
|
|
81
|
+
const value = await select({
|
|
82
|
+
message: question,
|
|
83
|
+
options: values.map(value => ({ value, label: value })),
|
|
84
|
+
initialValue: fallback
|
|
85
|
+
});
|
|
86
|
+
if (isCancel(value))
|
|
87
|
+
stop();
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function stop() {
|
|
91
|
+
cancel("No changes made");
|
|
92
|
+
throw new PromptCancelled();
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
interactive,
|
|
96
|
+
begin,
|
|
97
|
+
finish,
|
|
98
|
+
detail,
|
|
99
|
+
message,
|
|
100
|
+
progress,
|
|
101
|
+
ask,
|
|
102
|
+
yes,
|
|
103
|
+
choose
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/style.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* commands speaking three visual languages is worse than any one of
|
|
7
|
-
* them: a person reading `phresh install` after `phresh init` should not
|
|
8
|
-
* have to work out that it is the same tool.
|
|
9
|
-
*/
|
|
10
|
-
const colour = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
11
|
-
const escape = String.fromCharCode(27);
|
|
12
|
-
export const dim = (text) => colour ? `${escape}[2m${text}${escape}[22m` : text;
|
|
13
|
-
export const bold = (text) => colour ? `${escape}[1m${text}${escape}[22m` : text;
|
|
1
|
+
import colors from "picocolors";
|
|
2
|
+
/** The quiet and emphatic text used by non-interactive command reports. */
|
|
3
|
+
export const dim = colors.dim;
|
|
4
|
+
export const bold = colors.bold;
|
|
5
|
+
export const accent = colors.cyan;
|
|
14
6
|
// A label, what it says, and where that came from. The label is quiet
|
|
15
7
|
// and the value is not, because the value is the thing being reported.
|
|
16
8
|
//
|
|
@@ -18,7 +10,10 @@ export const bold = (text) => colour ? `${escape}[1m${text}${escape}[22m` : text
|
|
|
18
10
|
// even when one overruns: a label that runs into its value is worse than
|
|
19
11
|
// a column that does not line up.
|
|
20
12
|
export function line(label, value, note) {
|
|
21
|
-
console.log(` ${dim(label
|
|
13
|
+
console.log(` ${dim(column(label))}${value}${note ? ` ${dim(note)}` : ""}`);
|
|
14
|
+
}
|
|
15
|
+
export function column(label) {
|
|
16
|
+
return label.length < 12 ? label.padEnd(12) : `${label} `;
|
|
22
17
|
}
|
|
23
18
|
export function heading(title, note) {
|
|
24
19
|
console.log("");
|
package/dist/template/README.md
CHANGED
|
Binary file
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "phresh-program",
|
|
3
3
|
"private": true,
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"dev": "phresh dev",
|
|
8
|
+
"start": "phresh start"
|
|
8
9
|
},
|
|
9
10
|
"dependencies": {
|
|
10
|
-
"@phreshos/client": "^0.1.
|
|
11
|
-
"@phreshos/react": "^0.1.
|
|
12
|
-
"@phreshos/server": "^0.1.
|
|
13
|
-
"@phreshos/core": "^0.1.
|
|
11
|
+
"@phreshos/client": "^0.1.1",
|
|
12
|
+
"@phreshos/react": "^0.1.1",
|
|
13
|
+
"@phreshos/server": "^0.1.1",
|
|
14
|
+
"@phreshos/core": "^0.1.1",
|
|
14
15
|
"react": "^19.2.8",
|
|
15
16
|
"react-dom": "^19.2.8"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@phreshos/cli": "^0.1.
|
|
19
|
+
"@phreshos/cli": "^0.1.5",
|
|
19
20
|
"@types/node": "^26.2.0",
|
|
20
21
|
"@types/react": "^19.2.18",
|
|
21
22
|
"@types/react-dom": "^19.2.4",
|
|
@@ -1,69 +1,109 @@
|
|
|
1
1
|
import { defineConfig } from "@phreshos/core"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* This is the Program's authoring declaration, not a runtime configuration
|
|
5
|
+
* loaded by either endpoint. The Phresh CLI reads it and derives the concrete
|
|
6
|
+
* Program description needed for development, production, installation, or
|
|
7
|
+
* packaging.
|
|
8
|
+
*
|
|
9
|
+
* Production uses the built locations and commands declared below.
|
|
10
|
+
* Development replaces only each endpoint's location and start command with
|
|
11
|
+
* its `development` declaration. Packaging relocates the production files
|
|
12
|
+
* into the Program archive. Relative paths begin at this project directory.
|
|
13
|
+
*/
|
|
3
14
|
export default defineConfig({
|
|
4
15
|
|
|
5
|
-
//
|
|
6
|
-
|
|
16
|
+
// Permanent public address of the Program. It is kebab-case because the
|
|
17
|
+
// system also uses it as the installed directory name. Unlike `name`, it
|
|
18
|
+
// is an identifier and must remain stable across releases.
|
|
19
|
+
identity: "phresh-program",
|
|
7
20
|
|
|
8
|
-
// Human-facing
|
|
9
|
-
|
|
21
|
+
// Human-facing metadata shown by the desktop and authoring tools. None of
|
|
22
|
+
// these values determines the Program's identity.
|
|
23
|
+
name: "Phresh Program",
|
|
10
24
|
description: "A simple counter whose state lives on the Server.",
|
|
11
25
|
version: "0.1.0",
|
|
12
26
|
|
|
13
|
-
//
|
|
27
|
+
// Markdown entry point for the API owned by this Program. It documents the
|
|
28
|
+
// counter service contract; PhreshOS endpoint mechanics belong in PhreshOS
|
|
29
|
+
// documentation instead of being repeated here.
|
|
14
30
|
apiDocs: "api-docs.md",
|
|
15
31
|
|
|
16
|
-
//
|
|
17
|
-
|
|
32
|
+
// One authored PNG. Installation gives it a canonical name and the system
|
|
33
|
+
// derives the standard hosted icon sizes from it.
|
|
34
|
+
icon: "icon.png",
|
|
18
35
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
36
|
+
// Prepares both production endpoint directories. The CLI runs it from this
|
|
37
|
+
// project before `phresh start`, `phresh install`, and `phresh pack`.
|
|
38
|
+
// `phresh dev` does not build and uses the declarations below instead.
|
|
21
39
|
buildCommand: "node --import tsx source/build.ts",
|
|
22
40
|
|
|
41
|
+
// A Process may run its Server and Client independently. This declaration
|
|
42
|
+
// says how the Server is prepared and started; it does not merge Server
|
|
43
|
+
// code into the browser Client.
|
|
23
44
|
server: {
|
|
24
45
|
|
|
25
|
-
//
|
|
46
|
+
// Production directory containing the Server artifact. The start
|
|
47
|
+
// command runs with this directory as its working directory.
|
|
26
48
|
location: "dist/server",
|
|
27
49
|
startCommand: "node main.js",
|
|
28
50
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
51
|
+
// An install command is optional and runs inside `location` when the
|
|
52
|
+
// Program is installed. This build bundles its Server dependencies, so
|
|
53
|
+
// the example does not need one.
|
|
31
54
|
// installCommand: "npm install",
|
|
32
55
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
56
|
+
// Declared endpoints start in a default Process unless `start` is
|
|
57
|
+
// false. A false value keeps the capability available for an explicit
|
|
58
|
+
// Process launch without starting it automatically.
|
|
35
59
|
// start: false,
|
|
36
60
|
|
|
37
61
|
development: {
|
|
38
62
|
|
|
39
|
-
//
|
|
63
|
+
// `phresh dev` runs this command from the project directory. The
|
|
64
|
+
// project directory becomes the development Server location, so
|
|
65
|
+
// source imports and watch mode work without a production build.
|
|
40
66
|
startCommand: "node --watch --import tsx source/server/main.ts"
|
|
41
67
|
}
|
|
42
68
|
},
|
|
43
69
|
|
|
70
|
+
// The Client declaration also defines the initial Window created for it.
|
|
71
|
+
// It contains presentation defaults only; live Window state belongs to
|
|
72
|
+
// each running Process.
|
|
44
73
|
client: {
|
|
45
74
|
|
|
46
|
-
//
|
|
75
|
+
// Production directory containing the browser application's
|
|
76
|
+
// `index.html` and all files reachable from it.
|
|
47
77
|
location: "dist/client",
|
|
48
78
|
|
|
49
|
-
title
|
|
79
|
+
// Initial Window values. The title defaults to the Program name when
|
|
80
|
+
// omitted. This example chooses a fixed initial size while leaving
|
|
81
|
+
// placement, layer, and minimized state to their system defaults.
|
|
82
|
+
title: "Phresh Program",
|
|
50
83
|
size: { width: 600, height: 500 },
|
|
51
84
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// offsets may be combined with them in one linear expression.
|
|
85
|
+
// Geometry accepts finite pixel numbers or linear values. Fractions
|
|
86
|
+
// and percentages are relative to the selected desktop layer, and a
|
|
87
|
+
// pixel offset may be combined with either form.
|
|
56
88
|
// size: { width: "50% + 20", height: 440 },
|
|
57
89
|
// position: { x: "1/2 + 10", y: 40 },
|
|
58
|
-
|
|
90
|
+
|
|
91
|
+
// `window` is the ordinary framed layer. `under` and `over` are
|
|
92
|
+
// structurally isolated, frameless desktop layers.
|
|
93
|
+
// layer: "window",
|
|
94
|
+
|
|
95
|
+
// The initial Window may also be declared to open minimized.
|
|
59
96
|
// minimize: true,
|
|
60
97
|
|
|
61
98
|
development: {
|
|
62
99
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
100
|
+
// Development Clients are addressed by URL rather than a local
|
|
101
|
+
// artifact directory. The CLI starts this optional tool, waits for
|
|
102
|
+
// the URL to respond, and only then launches the Program. The dev
|
|
103
|
+
// server must allow the desktop origin through CORS; this project's
|
|
104
|
+
// Vite configuration does so.
|
|
65
105
|
url: "http://localhost:5200/",
|
|
66
|
-
startCommand: "vite dev"
|
|
106
|
+
startCommand: "vite dev --config vite.client.ts"
|
|
67
107
|
}
|
|
68
108
|
}
|
|
69
109
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { externalDependencies } from "@/vite.
|
|
1
|
+
import { externalDependencies } from "@/vite.server"
|
|
2
2
|
import { writeFile } from "node:fs/promises"
|
|
3
3
|
import packageConfig from "@/package.json"
|
|
4
4
|
import { build } from "vite"
|
|
@@ -10,8 +10,8 @@ for (const externalDependency of externalDependencies) {
|
|
|
10
10
|
dependencies[externalDependency] = packageConfig.dependencies[externalDependency]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
await build({
|
|
13
|
+
await build({ configFile: "vite.server.ts" })
|
|
14
14
|
|
|
15
|
-
await build()
|
|
15
|
+
await build({ configFile: "vite.client.ts" })
|
|
16
16
|
|
|
17
17
|
await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSubscribe } from "@phreshos/react"
|
|
2
2
|
import { useEffect, useState } from "react"
|
|
3
3
|
import { current } from "@phreshos/client"
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ function Counter() {
|
|
|
16
16
|
|
|
17
17
|
return <main className="counter-card">
|
|
18
18
|
|
|
19
|
-
<span className="label">
|
|
19
|
+
<span className="label">Phresh Program</span>
|
|
20
20
|
|
|
21
21
|
<h1>Server counter</h1>
|
|
22
22
|
|
|
@@ -32,10 +32,5 @@ function Counter() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export default function App() {
|
|
35
|
-
|
|
36
|
-
return <CurrentProvider waitServer fallback={<main className="loading">Connecting to the Server…</main>}>
|
|
37
|
-
|
|
38
|
-
<Counter />
|
|
39
|
-
|
|
40
|
-
</CurrentProvider>
|
|
35
|
+
return <Counter />
|
|
41
36
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react"
|
|
2
|
+
import { defineConfig } from "vite"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: "source/client",
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
base: "./",
|
|
9
|
+
resolve: {
|
|
10
|
+
tsconfigPaths: true,
|
|
11
|
+
dedupe: ["react"]
|
|
12
|
+
},
|
|
13
|
+
server: {
|
|
14
|
+
cors: true,
|
|
15
|
+
port: 5200,
|
|
16
|
+
strictPort: true
|
|
17
|
+
},
|
|
18
|
+
build: {
|
|
19
|
+
emptyOutDir: true,
|
|
20
|
+
outDir: resolve(import.meta.dirname, "dist/client")
|
|
21
|
+
}
|
|
22
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import packageConfig from "./package.json" with { type: "json" }
|
|
2
|
+
import { defineConfig } from "vite"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
|
|
5
|
+
export const externalDependencies: (keyof typeof packageConfig.dependencies)[] = [
|
|
6
|
+
|
|
7
|
+
// Add packages here that should NOT be bundled during server, Example: "sqlite3"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
root: "source/server",
|
|
12
|
+
resolve: {
|
|
13
|
+
tsconfigPaths: true
|
|
14
|
+
},
|
|
15
|
+
ssr: {
|
|
16
|
+
noExternal: true,
|
|
17
|
+
external: externalDependencies
|
|
18
|
+
},
|
|
19
|
+
build: {
|
|
20
|
+
ssr: true,
|
|
21
|
+
emptyOutDir: true,
|
|
22
|
+
outDir: resolve(import.meta.dirname, "dist/server"),
|
|
23
|
+
rolldownOptions: {
|
|
24
|
+
input: "main.ts"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
7
8
|
"compile": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
|
|
8
|
-
"build": "node --run compile && node dist/build-template.js",
|
|
9
|
+
"build": "node --run clean && node --run compile && node dist/build-template.js",
|
|
9
10
|
"prepack": "node --run build"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
@@ -16,8 +17,11 @@
|
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@
|
|
20
|
-
"
|
|
20
|
+
"@clack/prompts": "^1.7.0",
|
|
21
|
+
"@phreshos/core": "^0.1.1",
|
|
22
|
+
"adm-zip": "^0.6.0",
|
|
23
|
+
"commander": "^15.0.0",
|
|
24
|
+
"picocolors": "^1.1.1"
|
|
21
25
|
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@types/adm-zip": "^0.5.8",
|
package/dist/questions.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { bold, dim } from "./style.js";
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
3
|
-
/** One prompt language shared by every interactive command. */
|
|
4
|
-
export default function questions() {
|
|
5
|
-
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6
|
-
const readline = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
7
|
-
async function ask(explanation, question, fallback) {
|
|
8
|
-
if (!readline)
|
|
9
|
-
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
10
|
-
const suffix = fallback === undefined ? "" : ` ${dim(`(${fallback})`)}`;
|
|
11
|
-
console.log(` ${dim(explanation)}`);
|
|
12
|
-
const said = (await readline.question(` ${bold(question)}${suffix} `)).trim();
|
|
13
|
-
console.log("");
|
|
14
|
-
return said || fallback || "";
|
|
15
|
-
}
|
|
16
|
-
async function yes(explanation, question, fallback) {
|
|
17
|
-
return (await ask(explanation, question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
interactive,
|
|
21
|
-
ask,
|
|
22
|
-
yes,
|
|
23
|
-
close: () => readline?.close()
|
|
24
|
-
};
|
|
25
|
-
}
|
package/dist/relative-value.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reduces a strict linear expression into `span * relative + pixels`.
|
|
3
|
-
*
|
|
4
|
-
* Numbers are absolute values. Percentages and `n/d` fractions are relative
|
|
5
|
-
* values. Addition and subtraction combine terms; multiplication and division
|
|
6
|
-
* scale the preceding term by a number. No arbitrary code or CSS is accepted.
|
|
7
|
-
*/
|
|
8
|
-
export function parseRelativeValue(input) {
|
|
9
|
-
if (typeof input === "number")
|
|
10
|
-
return Number.isFinite(input) ? { relative: 0, pixels: clean(input) } : null;
|
|
11
|
-
if (typeof input !== "string" || input.trim().length === 0)
|
|
12
|
-
return null;
|
|
13
|
-
return new Reader(input).parse();
|
|
14
|
-
}
|
|
15
|
-
/** Returns whether a value belongs to the linear relative-value grammar. */
|
|
16
|
-
export function isRelativeValue(value) {
|
|
17
|
-
return parseRelativeValue(value) !== null;
|
|
18
|
-
}
|
|
19
|
-
class Reader {
|
|
20
|
-
position = 0;
|
|
21
|
-
source;
|
|
22
|
-
constructor(source) {
|
|
23
|
-
this.source = source;
|
|
24
|
-
}
|
|
25
|
-
parse() {
|
|
26
|
-
const value = { relative: 0, pixels: 0 };
|
|
27
|
-
let direction = this.sign();
|
|
28
|
-
while (this.position < this.source.length) {
|
|
29
|
-
const term = this.term();
|
|
30
|
-
if (!term)
|
|
31
|
-
return null;
|
|
32
|
-
value.relative += term.relative * direction;
|
|
33
|
-
value.pixels += term.pixels * direction;
|
|
34
|
-
if (!finite(value))
|
|
35
|
-
return null;
|
|
36
|
-
this.space();
|
|
37
|
-
if (this.position === this.source.length)
|
|
38
|
-
return { relative: clean(value.relative), pixels: clean(value.pixels) };
|
|
39
|
-
const operator = this.source[this.position];
|
|
40
|
-
if (operator !== "+" && operator !== "-")
|
|
41
|
-
return null;
|
|
42
|
-
this.position += 1;
|
|
43
|
-
direction = (operator === "+" ? 1 : -1) * this.sign();
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
term() {
|
|
48
|
-
const value = this.measurement();
|
|
49
|
-
if (!value)
|
|
50
|
-
return null;
|
|
51
|
-
while (true) {
|
|
52
|
-
this.space();
|
|
53
|
-
const operator = this.source[this.position];
|
|
54
|
-
if (operator !== "*" && operator !== "/")
|
|
55
|
-
return value;
|
|
56
|
-
this.position += 1;
|
|
57
|
-
const scalar = this.scalar();
|
|
58
|
-
if (scalar === null || operator === "/" && scalar === 0)
|
|
59
|
-
return null;
|
|
60
|
-
const factor = operator === "*" ? scalar : 1 / scalar;
|
|
61
|
-
value.relative *= factor;
|
|
62
|
-
value.pixels *= factor;
|
|
63
|
-
if (!finite(value))
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
measurement() {
|
|
68
|
-
this.space();
|
|
69
|
-
const remainder = this.source.slice(this.position);
|
|
70
|
-
const fraction = remainder.match(/^(\d+(?:\.\d+)?|\.\d+)\s*\/\s*(\d+(?:\.\d+)?|\.\d+)(?![\d.])/);
|
|
71
|
-
if (fraction) {
|
|
72
|
-
const denominator = Number(fraction[2]);
|
|
73
|
-
if (!denominator)
|
|
74
|
-
return null;
|
|
75
|
-
this.position += fraction[0].length;
|
|
76
|
-
return { relative: Number(fraction[1]) / denominator, pixels: 0 };
|
|
77
|
-
}
|
|
78
|
-
const percentage = remainder.match(/^(\d+(?:\.\d+)?|\.\d+)\s*%/);
|
|
79
|
-
if (percentage) {
|
|
80
|
-
this.position += percentage[0].length;
|
|
81
|
-
return { relative: Number(percentage[1]) / 100, pixels: 0 };
|
|
82
|
-
}
|
|
83
|
-
const number = this.number();
|
|
84
|
-
return number === null ? null : { relative: 0, pixels: number };
|
|
85
|
-
}
|
|
86
|
-
scalar() {
|
|
87
|
-
const direction = this.sign();
|
|
88
|
-
const value = this.number();
|
|
89
|
-
return value === null ? null : value * direction;
|
|
90
|
-
}
|
|
91
|
-
number() {
|
|
92
|
-
this.space();
|
|
93
|
-
const number = this.source.slice(this.position).match(/^(\d+(?:\.\d+)?|\.\d+)/)?.[0];
|
|
94
|
-
if (!number)
|
|
95
|
-
return null;
|
|
96
|
-
this.position += number.length;
|
|
97
|
-
return Number(number);
|
|
98
|
-
}
|
|
99
|
-
sign() {
|
|
100
|
-
this.space();
|
|
101
|
-
const sign = this.source[this.position];
|
|
102
|
-
if (sign !== "+" && sign !== "-")
|
|
103
|
-
return 1;
|
|
104
|
-
this.position += 1;
|
|
105
|
-
this.space();
|
|
106
|
-
return sign === "-" ? -1 : 1;
|
|
107
|
-
}
|
|
108
|
-
space() {
|
|
109
|
-
while (/\s/.test(this.source[this.position] ?? ""))
|
|
110
|
-
this.position += 1;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function finite(value) {
|
|
114
|
-
return Number.isFinite(value.relative) && Number.isFinite(value.pixels);
|
|
115
|
-
}
|
|
116
|
-
function clean(value) {
|
|
117
|
-
return Object.is(value, -0) ? 0 : value;
|
|
118
|
-
}
|
|
Binary file
|