@phreshos/cli 0.1.2 → 0.1.4
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 +28 -0
- package/dist/build-template.js +59 -0
- package/dist/cli.js +46 -5
- package/dist/create.js +130 -0
- package/dist/init.js +10 -23
- package/dist/project-dependency.js +99 -37
- package/dist/questions.js +25 -0
- package/dist/template/README.md +28 -0
- package/dist/template/api-docs.md +38 -0
- package/dist/template/gitignore +30 -0
- package/dist/template/icons/128x128.png +0 -0
- package/dist/template/package.json +27 -0
- package/dist/template/phresh.config.ts +69 -0
- package/dist/template/source/build.ts +17 -0
- package/dist/template/source/client/app.tsx +41 -0
- package/dist/template/source/client/dec.d.ts +1 -0
- package/dist/template/source/client/index.html +13 -0
- package/dist/template/source/client/main.tsx +8 -0
- package/dist/template/source/client/style.css +81 -0
- package/dist/template/source/server/dec.d.ts +1 -0
- package/dist/template/source/server/main.ts +14 -0
- package/dist/template/tsconfig.json +24 -0
- package/dist/template/vite.config.ts +38 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Its Program commands depend on `@phreshos/core`, and its runtime operations
|
|
|
14
14
|
require a compatible system installation.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
+
phresh create app # create a complete new Program
|
|
17
18
|
phresh init # describe this program, once
|
|
18
19
|
phresh pack # run the optional build, then package its result
|
|
19
20
|
phresh install # lay this program out on this machine
|
|
@@ -33,6 +34,33 @@ and has no word for a process, a window, a store or a setting. The system's
|
|
|
33
34
|
local intake accepts exactly the Program this project declares, and so does
|
|
34
35
|
this tool.
|
|
35
36
|
|
|
37
|
+
## create
|
|
38
|
+
|
|
39
|
+
`phresh create <directory>` creates a complete Server and Client Program from
|
|
40
|
+
the maintained Get Started project. Get Started is the only authored template:
|
|
41
|
+
the CLI build copies a filtered snapshot into the package, excluding build
|
|
42
|
+
output, storage, installed modules, lockfiles, and archives. The installed CLI
|
|
43
|
+
therefore creates projects offline without cloning a repository or maintaining
|
|
44
|
+
a second template by hand.
|
|
45
|
+
|
|
46
|
+
The directory name becomes the stable kebab-case Program identity. In a
|
|
47
|
+
terminal, `create` asks for the directory when it is omitted, the readable
|
|
48
|
+
Program name, and the package manager. With no terminal, the directory is the
|
|
49
|
+
first argument and every optional choice is named:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
phresh create status-board \
|
|
53
|
+
--name "Status Board" \
|
|
54
|
+
--package-manager npm
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Dependencies are installed by default. `--no-install` creates the same valid
|
|
58
|
+
project and leaves installation as the first reported next step. When the CLI
|
|
59
|
+
runs from this repository, generated SDK dependencies point to the sibling
|
|
60
|
+
dev-kits; a distributed CLI keeps the published versions embedded in its
|
|
61
|
+
template. Both choices pass through the same dependency resolver used by
|
|
62
|
+
`init`.
|
|
63
|
+
|
|
36
64
|
## Saying something to a program you start
|
|
37
65
|
|
|
38
66
|
```bash
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cpSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, relative, resolve, sep } from "node:path";
|
|
3
|
+
const source = resolve(import.meta.dirname, "..", "..", "get-started");
|
|
4
|
+
const output = resolve(import.meta.dirname, "template");
|
|
5
|
+
const excludedDirectories = new Set(["dist", "node_modules", "storage"]);
|
|
6
|
+
const excludedFiles = new Set([
|
|
7
|
+
".DS_Store",
|
|
8
|
+
"bun.lock",
|
|
9
|
+
"bun.lockb",
|
|
10
|
+
"package-lock.json",
|
|
11
|
+
"pnpm-lock.yaml",
|
|
12
|
+
"yarn.lock"
|
|
13
|
+
]);
|
|
14
|
+
if (!existsSync(source))
|
|
15
|
+
throw new Error(`The Get Started source is not there: ${source}`);
|
|
16
|
+
rmSync(output, { recursive: true, force: true });
|
|
17
|
+
cpSync(source, output, { recursive: true, filter: included });
|
|
18
|
+
renameSync(resolve(output, ".gitignore"), resolve(output, "gitignore"));
|
|
19
|
+
const manifestPath = resolve(output, "package.json");
|
|
20
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
21
|
+
const versions = workspaceVersions();
|
|
22
|
+
for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
|
|
23
|
+
if (!dependencies)
|
|
24
|
+
continue;
|
|
25
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
26
|
+
if (!range.startsWith("workspace:"))
|
|
27
|
+
continue;
|
|
28
|
+
const version = versions.get(name);
|
|
29
|
+
if (!version)
|
|
30
|
+
throw new Error(`The template depends on ${name}, but no matching workspace package exists`);
|
|
31
|
+
dependencies[name] = `^${version}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
|
|
35
|
+
function included(path) {
|
|
36
|
+
const local = relative(source, path);
|
|
37
|
+
if (!local)
|
|
38
|
+
return true;
|
|
39
|
+
const parts = local.split(sep);
|
|
40
|
+
if (parts.some(part => excludedDirectories.has(part)))
|
|
41
|
+
return false;
|
|
42
|
+
const name = basename(path);
|
|
43
|
+
return !excludedFiles.has(name) && !name.endsWith(".zip");
|
|
44
|
+
}
|
|
45
|
+
function workspaceVersions() {
|
|
46
|
+
const versions = new Map();
|
|
47
|
+
const devKit = resolve(source, "..");
|
|
48
|
+
for (const entry of readdirSync(devKit, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isDirectory())
|
|
50
|
+
continue;
|
|
51
|
+
const path = resolve(devKit, entry.name, "package.json");
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
continue;
|
|
54
|
+
const manifest = JSON.parse(readFileSync(path, "utf-8"));
|
|
55
|
+
if (manifest.name && manifest.version)
|
|
56
|
+
versions.set(manifest.name, manifest.version);
|
|
57
|
+
}
|
|
58
|
+
return versions;
|
|
59
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { bold, dim, heading } from "./style.js";
|
|
3
3
|
import metadata from "../package.json" with { type: "json" };
|
|
4
|
+
import create from "./create.js";
|
|
4
5
|
import install from "./install.js";
|
|
5
6
|
import launch from "./launch.js";
|
|
6
7
|
import init from "./init.js";
|
|
7
8
|
import pack from "./pack.js";
|
|
8
9
|
import uninstall from "./uninstall.js";
|
|
9
10
|
const { version } = metadata;
|
|
11
|
+
const coreRange = metadata.dependencies["@phreshos/core"];
|
|
10
12
|
/**
|
|
11
13
|
* One command system.
|
|
12
14
|
*
|
|
@@ -15,6 +17,30 @@ const { version } = metadata;
|
|
|
15
17
|
* behavior belongs to the command that needs it; parsing never guesses.
|
|
16
18
|
*/
|
|
17
19
|
const commands = [
|
|
20
|
+
{
|
|
21
|
+
name: "create",
|
|
22
|
+
summary: "create a new Program project",
|
|
23
|
+
detail: [
|
|
24
|
+
"Creates a complete Server and Client project from the maintained",
|
|
25
|
+
"Get Started template bundled with this CLI.",
|
|
26
|
+
"",
|
|
27
|
+
"In a terminal, an omitted directory and the readable Program name",
|
|
28
|
+
"are collected interactively. Automation supplies the directory as",
|
|
29
|
+
"the first argument and may use named options."
|
|
30
|
+
],
|
|
31
|
+
positionals: [{ name: "directory", required: false }],
|
|
32
|
+
options: [
|
|
33
|
+
{ name: "name", value: "name", summary: "human-readable Program name" },
|
|
34
|
+
{ name: "package-manager", value: "manager", summary: "bun, npm, pnpm, or yarn" },
|
|
35
|
+
{ name: "no-install", summary: "create files without installing dependencies" }
|
|
36
|
+
],
|
|
37
|
+
run: options => create({
|
|
38
|
+
directory: options.positionals[0],
|
|
39
|
+
name: text(options, "name"),
|
|
40
|
+
packageManager: packageManager(text(options, "package-manager")),
|
|
41
|
+
install: options["no-install"] !== true
|
|
42
|
+
})
|
|
43
|
+
},
|
|
18
44
|
{
|
|
19
45
|
name: "init",
|
|
20
46
|
summary: "initialize an existing Program project",
|
|
@@ -53,7 +79,7 @@ const commands = [
|
|
|
53
79
|
clientDevelopmentUrl: text(options, "client-development-url"),
|
|
54
80
|
clientDevelopmentStartCommand: text(options, "client-development-start-command"),
|
|
55
81
|
force: options.force === true
|
|
56
|
-
}, process.cwd(),
|
|
82
|
+
}, process.cwd(), coreRange)
|
|
57
83
|
},
|
|
58
84
|
{
|
|
59
85
|
name: "pack",
|
|
@@ -149,7 +175,7 @@ catch (error) {
|
|
|
149
175
|
process.exit(1);
|
|
150
176
|
}
|
|
151
177
|
function parse(command, args) {
|
|
152
|
-
const options = { run: {} };
|
|
178
|
+
const options = { run: {}, positionals: [] };
|
|
153
179
|
for (let index = 0; index < args.length; index += 1) {
|
|
154
180
|
const argument = args[index];
|
|
155
181
|
if (command.runOptions && argument.startsWith(runOptionPrefix)) {
|
|
@@ -163,8 +189,13 @@ function parse(command, args) {
|
|
|
163
189
|
options.run[name] = said.slice(at + 1);
|
|
164
190
|
continue;
|
|
165
191
|
}
|
|
166
|
-
if (!argument.startsWith("--"))
|
|
167
|
-
|
|
192
|
+
if (!argument.startsWith("--")) {
|
|
193
|
+
const positional = command.positionals?.[options.positionals.length];
|
|
194
|
+
if (!positional)
|
|
195
|
+
throw new Error(`${command.name} takes no more positional arguments, and I was given "${argument}"`);
|
|
196
|
+
options.positionals.push(argument);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
168
199
|
const at = argument.indexOf("=");
|
|
169
200
|
const name = argument.slice(2, at < 0 ? undefined : at);
|
|
170
201
|
const declared = command.options?.find(option => option.name === name);
|
|
@@ -183,12 +214,20 @@ function parse(command, args) {
|
|
|
183
214
|
throw new Error(`--${name} needs <${declared.value}>`);
|
|
184
215
|
options[name] = value;
|
|
185
216
|
}
|
|
217
|
+
const missing = command.positionals?.slice(options.positionals.length).find(positional => positional.required);
|
|
218
|
+
if (missing)
|
|
219
|
+
throw new Error(`${command.name} needs <${missing.name}>`);
|
|
186
220
|
return options;
|
|
187
221
|
}
|
|
188
222
|
function text(options, name) {
|
|
189
223
|
const value = options[name];
|
|
190
224
|
return typeof value === "string" ? value : undefined;
|
|
191
225
|
}
|
|
226
|
+
function packageManager(value) {
|
|
227
|
+
if (value === undefined || value === "bun" || value === "npm" || value === "pnpm" || value === "yarn")
|
|
228
|
+
return value;
|
|
229
|
+
throw new Error(`--package-manager must be bun, npm, pnpm, or yarn; received "${value}"`);
|
|
230
|
+
}
|
|
192
231
|
function usage() {
|
|
193
232
|
heading(`phresh ${version}`, "create and manage Programs");
|
|
194
233
|
for (const command of commands)
|
|
@@ -198,7 +237,9 @@ function usage() {
|
|
|
198
237
|
console.log("");
|
|
199
238
|
}
|
|
200
239
|
function about(command) {
|
|
201
|
-
|
|
240
|
+
const positionals = command.positionals?.map(positional => positional.required ? `<${positional.name}>` : `[${positional.name}]`).join(" ");
|
|
241
|
+
const signature = [`phresh ${command.name}`, positionals, command.options?.length || command.runOptions ? "[options]" : ""].filter(Boolean).join(" ");
|
|
242
|
+
heading(signature, command.summary);
|
|
202
243
|
for (const said of command.detail)
|
|
203
244
|
console.log(said ? ` ${said}` : "");
|
|
204
245
|
if (command.options?.length || command.runOptions) {
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { projectDependency, installProjectDependencies, projectPackageManager } from "./project-dependency.js";
|
|
2
|
+
import questions from "./questions.js";
|
|
3
|
+
import { dim, heading, line } from "./style.js";
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
6
|
+
/** Creates a complete Program from the bundled Get Started snapshot. */
|
|
7
|
+
export default async function create(options = {}, directory = process.cwd()) {
|
|
8
|
+
const prompts = questions();
|
|
9
|
+
try {
|
|
10
|
+
const requested = options.directory ?? (prompts.interactive
|
|
11
|
+
? await prompts.ask("A new directory will contain the complete Server and Client project.", "Where should the Program be created?", "my-program")
|
|
12
|
+
: undefined);
|
|
13
|
+
if (!requested)
|
|
14
|
+
throw new Error("create needs a <directory> when no terminal is attached");
|
|
15
|
+
const target = resolve(directory, requested);
|
|
16
|
+
const identity = basename(target);
|
|
17
|
+
if (!programIdentity.test(identity))
|
|
18
|
+
throw new Error(`The directory name must be a kebab-case Program identity; received "${identity}"`);
|
|
19
|
+
if (existsSync(target))
|
|
20
|
+
throw new Error(`The destination already exists: ${target}`);
|
|
21
|
+
const name = options.name ?? (prompts.interactive
|
|
22
|
+
? await prompts.ask("This name is shown to people; the directory name remains the Program identity.", "What name should people see?", title(identity))
|
|
23
|
+
: title(identity));
|
|
24
|
+
if (!name.trim())
|
|
25
|
+
throw new Error("--name must not be empty");
|
|
26
|
+
const install = options.install !== false;
|
|
27
|
+
const detected = projectPackageManager(directory).name;
|
|
28
|
+
const manager = options.packageManager ?? (install && prompts.interactive
|
|
29
|
+
? packageManager(await prompts.ask("The generated project remains portable; this choice installs its dependencies now.", "Which package manager should be used?", detected))
|
|
30
|
+
: detected);
|
|
31
|
+
heading("Create Program", name);
|
|
32
|
+
line("identity", identity);
|
|
33
|
+
line("directory", target);
|
|
34
|
+
line("template", "Get Started");
|
|
35
|
+
if (install)
|
|
36
|
+
line("packages", manager);
|
|
37
|
+
console.log("");
|
|
38
|
+
const parent = dirname(target);
|
|
39
|
+
mkdirSync(parent, { recursive: true });
|
|
40
|
+
const staging = mkdtempSync(resolve(parent, `.${identity}-`));
|
|
41
|
+
try {
|
|
42
|
+
cpSync(template(), staging, { recursive: true });
|
|
43
|
+
renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
|
|
44
|
+
customize(staging, target, identity, name, manager);
|
|
45
|
+
if (install)
|
|
46
|
+
await installProjectDependencies(staging, manager);
|
|
47
|
+
renameSync(staging, target);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
rmSync(staging, { recursive: true, force: true });
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
heading(name, "created");
|
|
54
|
+
line("directory", target);
|
|
55
|
+
line("packages", install ? "installed" : "not installed");
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(` ${dim("Next:")} cd ${relative(directory, target) || "."}`);
|
|
58
|
+
if (!install)
|
|
59
|
+
console.log(` ${manager} install`);
|
|
60
|
+
console.log(" phresh dev");
|
|
61
|
+
console.log("");
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
prompts.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function template() {
|
|
68
|
+
const candidates = [
|
|
69
|
+
resolve(import.meta.dirname, "template"),
|
|
70
|
+
resolve(import.meta.dirname, "..", "dist", "template")
|
|
71
|
+
];
|
|
72
|
+
const found = candidates.find(existsSync);
|
|
73
|
+
if (!found)
|
|
74
|
+
throw new Error("The CLI template has not been built — run its build command and try again");
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
function customize(directory, finalDirectory, identity, name, manager) {
|
|
78
|
+
for (const path of textFiles(directory)) {
|
|
79
|
+
let content = readFileSync(path, "utf-8");
|
|
80
|
+
content = content.replaceAll("get-started", identity).replaceAll("Get Started", name);
|
|
81
|
+
if (basename(path) === "README.md") {
|
|
82
|
+
content = content
|
|
83
|
+
.replaceAll("bun install", `${manager} install`)
|
|
84
|
+
.replaceAll("bun phresh ", "phresh ");
|
|
85
|
+
}
|
|
86
|
+
writeFileSync(path, content);
|
|
87
|
+
}
|
|
88
|
+
const manifestPath = resolve(directory, "package.json");
|
|
89
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
90
|
+
manifest.name = identity;
|
|
91
|
+
for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
|
|
92
|
+
if (!dependencies)
|
|
93
|
+
continue;
|
|
94
|
+
for (const [dependency, range] of Object.entries(dependencies)) {
|
|
95
|
+
if (!isProjectPackage(dependency))
|
|
96
|
+
continue;
|
|
97
|
+
dependencies[dependency] = projectDependency(dependency, range, finalDirectory).manifestSpecifier;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
|
|
101
|
+
}
|
|
102
|
+
function textFiles(directory) {
|
|
103
|
+
const files = [];
|
|
104
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
105
|
+
const path = resolve(directory, entry.name);
|
|
106
|
+
if (entry.isDirectory())
|
|
107
|
+
files.push(...textFiles(path));
|
|
108
|
+
else if (entry.isFile() && (textExtensions.has(extname(entry.name)) || textNames.has(entry.name)))
|
|
109
|
+
files.push(path);
|
|
110
|
+
}
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
113
|
+
function packageManager(value) {
|
|
114
|
+
if (value === "bun" || value === "npm" || value === "pnpm" || value === "yarn")
|
|
115
|
+
return value;
|
|
116
|
+
throw new Error(`The package manager must be bun, npm, pnpm, or yarn; received "${value}"`);
|
|
117
|
+
}
|
|
118
|
+
function isProjectPackage(value) {
|
|
119
|
+
return value === "@phreshos/core"
|
|
120
|
+
|| value === "@phreshos/client"
|
|
121
|
+
|| value === "@phreshos/server"
|
|
122
|
+
|| value === "@phreshos/react"
|
|
123
|
+
|| value === "@phreshos/cli";
|
|
124
|
+
}
|
|
125
|
+
function title(identity) {
|
|
126
|
+
return identity.split("-").map(word => word[0].toUpperCase() + word.slice(1)).join(" ");
|
|
127
|
+
}
|
|
128
|
+
const programIdentity = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
129
|
+
const textExtensions = new Set([".css", ".html", ".json", ".md", ".ts", ".tsx"]);
|
|
130
|
+
const textNames = new Set([".gitignore", "gitignore"]);
|
package/dist/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {} from "@phreshos/core";
|
|
2
2
|
import { configFile, readManifest } from "./project.js";
|
|
3
|
-
import {
|
|
3
|
+
import { dim, heading, line } from "./style.js";
|
|
4
4
|
import ensureProjectDependency, { projectScript } from "./project-dependency.js";
|
|
5
|
-
import
|
|
5
|
+
import questions from "./questions.js";
|
|
6
6
|
import { existsSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
/**
|
|
@@ -13,26 +13,14 @@ import { resolve } from "node:path";
|
|
|
13
13
|
* same config and ensure the matching Core dependency through this one
|
|
14
14
|
* function.
|
|
15
15
|
*/
|
|
16
|
-
export default async function init(options = {}, directory = process.cwd(),
|
|
16
|
+
export default async function init(options = {}, directory = process.cwd(), coreRange = "^0.1.0") {
|
|
17
17
|
const manifest = await readManifest(directory);
|
|
18
18
|
const path = resolve(directory, configFile);
|
|
19
|
-
const
|
|
19
|
+
const prompts = questions();
|
|
20
|
+
const { interactive, ask, yes } = prompts;
|
|
20
21
|
const explicitShape = options.server === true || options.client === true;
|
|
21
22
|
if (existsSync(path) && options.force !== true && !interactive)
|
|
22
23
|
throw new Error(`${configFile} already exists — use --force to replace it`);
|
|
23
|
-
const readline = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
24
|
-
async function ask(explanation, question, fallback) {
|
|
25
|
-
if (!readline)
|
|
26
|
-
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
27
|
-
const suffix = fallback === undefined ? "" : ` ${dim(`(${fallback})`)}`;
|
|
28
|
-
console.log(` ${dim(explanation)}`);
|
|
29
|
-
const said = (await readline.question(` ${bold(question)}${suffix} `)).trim();
|
|
30
|
-
console.log("");
|
|
31
|
-
return said || fallback || "";
|
|
32
|
-
}
|
|
33
|
-
async function yes(explanation, question, fallback) {
|
|
34
|
-
return (await ask(explanation, question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
|
|
35
|
-
}
|
|
36
24
|
try {
|
|
37
25
|
if (existsSync(path) && options.force !== true) {
|
|
38
26
|
heading(configFile, "already initialized");
|
|
@@ -138,7 +126,7 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
138
126
|
const config = server
|
|
139
127
|
? { ...described, server, ...client && { client } }
|
|
140
128
|
: { ...described, client: client };
|
|
141
|
-
await ensureProjectDependency("@phreshos/core",
|
|
129
|
+
await ensureProjectDependency("@phreshos/core", coreRange, directory);
|
|
142
130
|
writeFileSync(path, compose(config));
|
|
143
131
|
heading(configFile, "created");
|
|
144
132
|
if (config.server)
|
|
@@ -159,7 +147,7 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
159
147
|
console.log("");
|
|
160
148
|
}
|
|
161
149
|
finally {
|
|
162
|
-
|
|
150
|
+
prompts.close();
|
|
163
151
|
}
|
|
164
152
|
}
|
|
165
153
|
function compose(config) {
|
|
@@ -184,8 +172,7 @@ function compose(config) {
|
|
|
184
172
|
`import { defineConfig } from "@phreshos/core"`,
|
|
185
173
|
"",
|
|
186
174
|
"export default defineConfig({",
|
|
187
|
-
"",
|
|
188
|
-
blocks.filter(Boolean).join(",\n\n"),
|
|
175
|
+
blocks.filter(Boolean).join(",\n"),
|
|
189
176
|
"})",
|
|
190
177
|
""
|
|
191
178
|
].join("\n");
|
|
@@ -200,9 +187,9 @@ function half(name, values) {
|
|
|
200
187
|
if (typeof value === "string")
|
|
201
188
|
return ` ${key}: ${JSON.stringify(value)}`;
|
|
202
189
|
const nested = Object.entries(value).map(([nestedKey, nestedValue]) => ` ${nestedKey}: ${JSON.stringify(nestedValue)}`);
|
|
203
|
-
return ` ${key}: {\n
|
|
190
|
+
return ` ${key}: {\n${nested.join(",\n")}\n }`;
|
|
204
191
|
});
|
|
205
|
-
return ` ${name}: {\n
|
|
192
|
+
return ` ${name}: {\n${inside.join(",\n")}\n }`;
|
|
206
193
|
}
|
|
207
194
|
function httpUrl(value) {
|
|
208
195
|
try {
|
|
@@ -1,51 +1,86 @@
|
|
|
1
1
|
import { line } from "./style.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { relative, resolve } from "node:path";
|
|
5
5
|
/** Returns the package-manager command that runs one project script. */
|
|
6
6
|
export function projectScript(directory, declared, script) {
|
|
7
|
-
return `${
|
|
7
|
+
return `${projectPackageManager(directory, declared).name} run ${script}`;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Resolves an SDK through the one source policy shared by `init` and `create`.
|
|
11
11
|
*
|
|
12
|
-
* A CLI running from this repository uses its sibling dev-kit
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* decision belongs here once for both `init` and the future `create` command.
|
|
12
|
+
* A CLI running from this repository uses its sibling dev-kit. A distributed
|
|
13
|
+
* CLI has no such sibling and keeps the published range embedded in its
|
|
14
|
+
* generated template.
|
|
16
15
|
*/
|
|
17
|
-
export
|
|
16
|
+
export function projectDependency(name, range, directory) {
|
|
17
|
+
const workspace = manifestAt(resolve(import.meta.dirname, "..", "..", ".."));
|
|
18
|
+
const sourceDirectory = resolve(import.meta.dirname, "..", "..", localDirectories[name]);
|
|
19
|
+
const manifest = manifestAt(sourceDirectory);
|
|
20
|
+
if (workspace?.name === "@phreshos/workspace" && manifest?.name === name && manifest.version && accepts(range, manifest.version)) {
|
|
21
|
+
const local = relative(directory, sourceDirectory).replaceAll("\\", "/") || ".";
|
|
22
|
+
return {
|
|
23
|
+
installSpecifier: sourceDirectory,
|
|
24
|
+
manifestSpecifier: `file:${local}`,
|
|
25
|
+
local: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
installSpecifier: `${name}@${range}`,
|
|
30
|
+
manifestSpecifier: range,
|
|
31
|
+
local: false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Detects the package manager declared by, present in, or invoking a project. */
|
|
35
|
+
export function projectPackageManager(directory, declared, preferred) {
|
|
36
|
+
const named = preferred ?? packageManagerName(declared) ?? packageManagerFromLocks(directory) ?? packageManagerFromInvocation();
|
|
37
|
+
return managers[named ?? "npm"];
|
|
38
|
+
}
|
|
39
|
+
/** Installs the dependencies already declared by a generated project. */
|
|
40
|
+
export async function installProjectDependencies(directory, preferred) {
|
|
41
|
+
const manifest = manifestAt(directory);
|
|
42
|
+
const manager = projectPackageManager(directory, manifest?.packageManager, preferred);
|
|
43
|
+
await run(manager.name, manager.installArgs, directory);
|
|
44
|
+
return manager.name;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ensures one project dependency using the source appropriate to this CLI.
|
|
48
|
+
*
|
|
49
|
+
* `init` uses development dependencies because Core supplies the authoring
|
|
50
|
+
* contract for an existing project. `create` preserves the canonical
|
|
51
|
+
* template's own dependency sections instead.
|
|
52
|
+
*/
|
|
53
|
+
export default async function ensureProjectDependency(name, range, directory = process.cwd(), section = "devDependencies") {
|
|
18
54
|
const manifestPath = resolve(directory, "package.json");
|
|
19
55
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
20
56
|
if (manifest.dependencies?.[name] || manifest.devDependencies?.[name])
|
|
21
57
|
return;
|
|
22
|
-
const manager =
|
|
23
|
-
const source =
|
|
24
|
-
line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" :
|
|
25
|
-
await
|
|
26
|
-
|
|
58
|
+
const manager = projectPackageManager(directory, manifest.packageManager);
|
|
59
|
+
const source = projectDependency(name, range, directory);
|
|
60
|
+
line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" : range}`);
|
|
61
|
+
await run(manager.name, [...manager.addArgs(section), source.installSpecifier], directory);
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
function run(command, args, directory) {
|
|
65
|
+
return new Promise(function (settle, refuse) {
|
|
66
|
+
const child = spawn(command, args, {
|
|
27
67
|
cwd: directory,
|
|
28
68
|
stdio: "inherit",
|
|
29
69
|
shell: process.platform === "win32"
|
|
30
70
|
});
|
|
31
|
-
child.once("error", error => refuse(new Error(`Could not run ${
|
|
71
|
+
child.once("error", error => refuse(new Error(`Could not run ${command}: ${error.message}`)));
|
|
32
72
|
child.once("exit", function (code, signal) {
|
|
33
73
|
if (signal)
|
|
34
|
-
refuse(new Error(`${
|
|
74
|
+
refuse(new Error(`${command} ended on ${signal}`));
|
|
35
75
|
else if (code !== 0)
|
|
36
|
-
refuse(new Error(`${
|
|
76
|
+
refuse(new Error(`${command} exited with ${code ?? 0}`));
|
|
37
77
|
else
|
|
38
78
|
settle();
|
|
39
79
|
});
|
|
40
80
|
});
|
|
41
|
-
console.log("");
|
|
42
81
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
const manifest = manifestAt(directory);
|
|
46
|
-
if (manifest?.name === name && manifest.version === version)
|
|
47
|
-
return { specifier: directory, local: true };
|
|
48
|
-
return { specifier: `${name}@^${version}`, local: false };
|
|
82
|
+
function accepts(range, version) {
|
|
83
|
+
return range === "workspace:*" || range === version || range === `^${version}` || range === `~${version}`;
|
|
49
84
|
}
|
|
50
85
|
function manifestAt(directory) {
|
|
51
86
|
const path = resolve(directory, "package.json");
|
|
@@ -58,26 +93,53 @@ function manifestAt(directory) {
|
|
|
58
93
|
return null;
|
|
59
94
|
}
|
|
60
95
|
}
|
|
61
|
-
function
|
|
96
|
+
function packageManagerName(declared) {
|
|
62
97
|
const named = declared?.split("@")[0];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { name: "pnpm", args: ["add", "--save-dev"] };
|
|
67
|
-
if (named === "yarn")
|
|
68
|
-
return { name: "yarn", args: ["add", "--dev"] };
|
|
69
|
-
if (named === "npm")
|
|
70
|
-
return { name: "npm", args: ["install", "--save-dev", "--no-fund", "--no-audit"] };
|
|
98
|
+
return isPackageManager(named) ? named : undefined;
|
|
99
|
+
}
|
|
100
|
+
function packageManagerFromLocks(directory) {
|
|
71
101
|
if (existsSync(resolve(directory, "bun.lock")) || existsSync(resolve(directory, "bun.lockb")))
|
|
72
|
-
return
|
|
102
|
+
return "bun";
|
|
73
103
|
if (existsSync(resolve(directory, "pnpm-lock.yaml")))
|
|
74
|
-
return
|
|
104
|
+
return "pnpm";
|
|
75
105
|
if (existsSync(resolve(directory, "yarn.lock")))
|
|
76
|
-
return
|
|
77
|
-
|
|
106
|
+
return "yarn";
|
|
107
|
+
if (existsSync(resolve(directory, "package-lock.json")))
|
|
108
|
+
return "npm";
|
|
109
|
+
}
|
|
110
|
+
function packageManagerFromInvocation() {
|
|
111
|
+
const named = process.env.npm_config_user_agent?.split("/")[0];
|
|
112
|
+
return isPackageManager(named) ? named : undefined;
|
|
113
|
+
}
|
|
114
|
+
function isPackageManager(value) {
|
|
115
|
+
return value === "bun" || value === "npm" || value === "pnpm" || value === "yarn";
|
|
78
116
|
}
|
|
79
117
|
const localDirectories = {
|
|
80
118
|
"@phreshos/core": "core-sdk",
|
|
81
119
|
"@phreshos/client": "client-sdk",
|
|
82
|
-
"@phreshos/server": "server-sdk"
|
|
120
|
+
"@phreshos/server": "server-sdk",
|
|
121
|
+
"@phreshos/react": "react-sdk",
|
|
122
|
+
"@phreshos/cli": "cli"
|
|
123
|
+
};
|
|
124
|
+
const managers = {
|
|
125
|
+
bun: {
|
|
126
|
+
name: "bun",
|
|
127
|
+
installArgs: ["install"],
|
|
128
|
+
addArgs: section => ["add", ...section === "devDependencies" ? ["--dev"] : []]
|
|
129
|
+
},
|
|
130
|
+
npm: {
|
|
131
|
+
name: "npm",
|
|
132
|
+
installArgs: ["install", "--no-fund", "--no-audit"],
|
|
133
|
+
addArgs: section => ["install", ...section === "devDependencies" ? ["--save-dev"] : ["--save"], "--no-fund", "--no-audit"]
|
|
134
|
+
},
|
|
135
|
+
pnpm: {
|
|
136
|
+
name: "pnpm",
|
|
137
|
+
installArgs: ["install"],
|
|
138
|
+
addArgs: section => ["add", ...section === "devDependencies" ? ["--save-dev"] : ["--save-prod"]]
|
|
139
|
+
},
|
|
140
|
+
yarn: {
|
|
141
|
+
name: "yarn",
|
|
142
|
+
installArgs: ["install"],
|
|
143
|
+
addArgs: section => ["add", ...section === "devDependencies" ? ["--dev"] : []]
|
|
144
|
+
}
|
|
83
145
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Get Started
|
|
2
|
+
|
|
3
|
+
A small Counter built as a complete Program. Its Node.js Server owns the
|
|
4
|
+
number, while its React Client reads and increments that same authoritative
|
|
5
|
+
state.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun phresh dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Development starts the Server source and Vite Client together. For the
|
|
13
|
+
production shape, build and attach the Program with:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun phresh start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The structure follows the endpoint boundaries directly:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
source/
|
|
23
|
+
├── client/ React interface
|
|
24
|
+
└── server/ authoritative counter and API
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Program declaration lives in `phresh.config.ts`. Its own service contract
|
|
28
|
+
is documented in `api-docs.md`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Counter API
|
|
2
|
+
|
|
3
|
+
The Server owns one number for the lifetime of its Process.
|
|
4
|
+
|
|
5
|
+
| Name | Destination | Interaction | Payload | Answer |
|
|
6
|
+
| --- | --- | --- | --- | --- |
|
|
7
|
+
| `read` | Server | `ask()` | `undefined` | `number` |
|
|
8
|
+
| `increment` | Server | `publish()` | `undefined` | None |
|
|
9
|
+
| `changed` | Server | `subscribe()` | `number` | None |
|
|
10
|
+
|
|
11
|
+
## Ask the Server: `read`
|
|
12
|
+
|
|
13
|
+
`read` is a question addressed to this Process's Server. It answers with the
|
|
14
|
+
current counter value.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const count = await process.server.ask<number>("read")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Publish to the Server: `increment`
|
|
21
|
+
|
|
22
|
+
`increment` is a one-way event addressed to this Process's Server. It adds one
|
|
23
|
+
to the counter and does not produce an answer.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
process.server.publish("increment")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Subscribe to the Server: `changed`
|
|
30
|
+
|
|
31
|
+
After an increment, the Server emits `changed` with the new counter value.
|
|
32
|
+
Anyone holding that Process's Server can subscribe to the event.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
process.server.subscribe<number>("changed", count => {
|
|
36
|
+
// use the latest count
|
|
37
|
+
})
|
|
38
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Made by `phresh pack` in the project root.
|
|
16
|
+
/*.zip
|
|
17
|
+
|
|
18
|
+
# Editor directories and files
|
|
19
|
+
.vscode/*
|
|
20
|
+
!.vscode/extensions.json
|
|
21
|
+
.idea
|
|
22
|
+
.DS_Store
|
|
23
|
+
*.suo
|
|
24
|
+
*.ntvs*
|
|
25
|
+
*.njsproj
|
|
26
|
+
*.sln
|
|
27
|
+
*.sw?
|
|
28
|
+
|
|
29
|
+
# Made by the system the first time this program runs.
|
|
30
|
+
storage
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "get-started",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"phresh": "phresh"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@phreshos/client": "^0.1.0",
|
|
11
|
+
"@phreshos/react": "^0.1.0",
|
|
12
|
+
"@phreshos/server": "^0.1.0",
|
|
13
|
+
"@phreshos/core": "^0.1.0",
|
|
14
|
+
"react": "^19.2.8",
|
|
15
|
+
"react-dom": "^19.2.8"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@phreshos/cli": "^0.1.4",
|
|
19
|
+
"@types/node": "^26.2.0",
|
|
20
|
+
"@types/react": "^19.2.18",
|
|
21
|
+
"@types/react-dom": "^19.2.4",
|
|
22
|
+
"@vitejs/plugin-react": "^6.0.5",
|
|
23
|
+
"tsx": "^4.23.12",
|
|
24
|
+
"typescript": "^7.0.2",
|
|
25
|
+
"vite": "^8.2.1"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineConfig } from "@phreshos/core"
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
|
|
5
|
+
// The stable, kebab-case address used by programs and the system.
|
|
6
|
+
identity: "get-started",
|
|
7
|
+
|
|
8
|
+
// Human-facing details shown by the desktop and development tools.
|
|
9
|
+
name: "Get Started",
|
|
10
|
+
description: "A simple counter whose state lives on the Server.",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
|
|
13
|
+
// The official entry point for this Program's own API.
|
|
14
|
+
apiDocs: "api-docs.md",
|
|
15
|
+
|
|
16
|
+
// A directory containing files named by size, such as 128x128.png.
|
|
17
|
+
icons: "icons",
|
|
18
|
+
|
|
19
|
+
// Runs before production start, install, and pack; dev uses the
|
|
20
|
+
// development declarations below instead.
|
|
21
|
+
buildCommand: "node --import tsx source/build.ts",
|
|
22
|
+
|
|
23
|
+
server: {
|
|
24
|
+
|
|
25
|
+
// The built server directory and the command run from within it.
|
|
26
|
+
location: "dist/server",
|
|
27
|
+
startCommand: "node main.js",
|
|
28
|
+
|
|
29
|
+
// Optional preparation performed once before the installed server
|
|
30
|
+
// first starts. This bundled example does not need it.
|
|
31
|
+
// installCommand: "npm install",
|
|
32
|
+
|
|
33
|
+
// A declared half starts by default. Set this to false when it
|
|
34
|
+
// should exist but only be included when explicitly requested.
|
|
35
|
+
// start: false,
|
|
36
|
+
|
|
37
|
+
development: {
|
|
38
|
+
|
|
39
|
+
// Development server commands run from the project root.
|
|
40
|
+
startCommand: "node --watch --import tsx source/server/main.ts"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
client: {
|
|
45
|
+
|
|
46
|
+
// The directory containing the built browser application.
|
|
47
|
+
location: "dist/client",
|
|
48
|
+
|
|
49
|
+
title: "Get Started",
|
|
50
|
+
size: { width: 600, height: 500 },
|
|
51
|
+
|
|
52
|
+
// Optional defaults for the window created with this client half.
|
|
53
|
+
// title: "Get Started",
|
|
54
|
+
// Fractions and percentages are relative to the Window's layer. Pixel
|
|
55
|
+
// offsets may be combined with them in one linear expression.
|
|
56
|
+
// size: { width: "50% + 20", height: 440 },
|
|
57
|
+
// position: { x: "1/2 + 10", y: 40 },
|
|
58
|
+
// layer: "window", // "under" and "over" are frameless desktop layers.
|
|
59
|
+
// minimize: true,
|
|
60
|
+
|
|
61
|
+
development: {
|
|
62
|
+
|
|
63
|
+
// The CLI starts this command, then waits for the URL before it
|
|
64
|
+
// launches the development Program.
|
|
65
|
+
url: "http://localhost:5200/",
|
|
66
|
+
startCommand: "vite dev"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { externalDependencies } from "@/vite.config"
|
|
2
|
+
import { writeFile } from "node:fs/promises"
|
|
3
|
+
import packageConfig from "@/package.json"
|
|
4
|
+
import { build } from "vite"
|
|
5
|
+
|
|
6
|
+
const dependencies: Partial<typeof packageConfig.dependencies> = {}
|
|
7
|
+
|
|
8
|
+
for (const externalDependency of externalDependencies) {
|
|
9
|
+
|
|
10
|
+
dependencies[externalDependency] = packageConfig.dependencies[externalDependency]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await build({ build: { ssr: true } })
|
|
14
|
+
|
|
15
|
+
await build()
|
|
16
|
+
|
|
17
|
+
await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CurrentProvider, useSubscribe } from "@phreshos/react"
|
|
2
|
+
import { useEffect, useState } from "react"
|
|
3
|
+
import { current } from "@phreshos/client"
|
|
4
|
+
|
|
5
|
+
function Counter() {
|
|
6
|
+
|
|
7
|
+
const [count, setCount] = useState<number>()
|
|
8
|
+
|
|
9
|
+
useSubscribe(current.server, "changed", setCount)
|
|
10
|
+
|
|
11
|
+
useEffect(function () {
|
|
12
|
+
|
|
13
|
+
current.server.ask<number>("read").then(setCount)
|
|
14
|
+
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
return <main className="counter-card">
|
|
18
|
+
|
|
19
|
+
<span className="label">Get Started</span>
|
|
20
|
+
|
|
21
|
+
<h1>Server counter</h1>
|
|
22
|
+
|
|
23
|
+
<p>The value belongs to the Server and is shared with every Client representation.</p>
|
|
24
|
+
|
|
25
|
+
<button type="button" onClick={() => current.server.publish("increment")}>
|
|
26
|
+
|
|
27
|
+
count is {count ?? "…"}
|
|
28
|
+
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
</main>
|
|
32
|
+
}
|
|
33
|
+
|
|
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>
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #20242a;
|
|
3
|
+
background: #f3f4f6;
|
|
4
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
|
|
10
|
+
html, body, #root { min-height: 100vh; display: grid; }
|
|
11
|
+
|
|
12
|
+
body { margin: 0; }
|
|
13
|
+
|
|
14
|
+
button { font: inherit; }
|
|
15
|
+
|
|
16
|
+
#root {
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
display: grid;
|
|
19
|
+
place-items: center;
|
|
20
|
+
padding: 1.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.counter-card {
|
|
24
|
+
width: min(26rem, 100%);
|
|
25
|
+
display: grid;
|
|
26
|
+
justify-items: start;
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
border: 1px solid #d8dce2;
|
|
30
|
+
border-radius: 0.75rem;
|
|
31
|
+
background: #ffffff;
|
|
32
|
+
margin: auto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.label {
|
|
36
|
+
color: #646b76;
|
|
37
|
+
font-size: 0.72rem;
|
|
38
|
+
font-weight: 700;
|
|
39
|
+
letter-spacing: 0.1em;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h1 {
|
|
44
|
+
margin: 0;
|
|
45
|
+
font-size: 2rem;
|
|
46
|
+
line-height: 1.1;
|
|
47
|
+
letter-spacing: -0.04em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
p {
|
|
51
|
+
margin: 0;
|
|
52
|
+
color: #5f6670;
|
|
53
|
+
font-size: 0.95rem;
|
|
54
|
+
line-height: 1.55;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.counter-card button {
|
|
58
|
+
min-width: 9rem;
|
|
59
|
+
min-height: 2.75rem;
|
|
60
|
+
padding: 0.65rem 1rem;
|
|
61
|
+
border: 0;
|
|
62
|
+
border-radius: 0.5rem;
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
background: #2563eb;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
font-weight: 650;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.counter-card button:hover { background: #1d4ed8; }
|
|
70
|
+
|
|
71
|
+
.counter-card button:active { background: #1e40af; }
|
|
72
|
+
|
|
73
|
+
.counter-card button:focus-visible {
|
|
74
|
+
outline: 3px solid rgba(37, 99, 235, 0.25);
|
|
75
|
+
outline-offset: 2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.loading {
|
|
79
|
+
color: #646b76;
|
|
80
|
+
font-size: 0.9rem;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@types/node" />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { current } from "@phreshos/server"
|
|
2
|
+
|
|
3
|
+
// The Server owns the value. Every Client representation reads and changes
|
|
4
|
+
// this same state instead of keeping an independent browser counter.
|
|
5
|
+
let count = 0
|
|
6
|
+
|
|
7
|
+
current.answer("read", () => count)
|
|
8
|
+
|
|
9
|
+
current.subscribe("increment", function () {
|
|
10
|
+
|
|
11
|
+
count += 1
|
|
12
|
+
|
|
13
|
+
current.publish("changed", count)
|
|
14
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"moduleResolution": "bundler",
|
|
4
|
+
"noUnusedParameters": true,
|
|
5
|
+
"noUnusedLocals": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"target": "ESNext",
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"paths": {
|
|
13
|
+
"@/*": [
|
|
14
|
+
"./*"
|
|
15
|
+
],
|
|
16
|
+
"@server/*": [
|
|
17
|
+
"./source/server/*"
|
|
18
|
+
],
|
|
19
|
+
"@client/*": [
|
|
20
|
+
"./source/client/*"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import packageConfig from "./package.json" with { type: "json" }
|
|
2
|
+
import react from "@vitejs/plugin-react"
|
|
3
|
+
import { defineConfig } from "vite"
|
|
4
|
+
import { resolve } from "node:path"
|
|
5
|
+
|
|
6
|
+
export const externalDependencies: (keyof typeof packageConfig.dependencies)[] = [
|
|
7
|
+
|
|
8
|
+
// Add packages here that should NOT be bundled during server, Example: "sqlite3"
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export default defineConfig(function (config) {
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
base: "./",
|
|
15
|
+
resolve: {
|
|
16
|
+
tsconfigPaths: true,
|
|
17
|
+
dedupe: ["react"]
|
|
18
|
+
},
|
|
19
|
+
server: {
|
|
20
|
+
cors: true,
|
|
21
|
+
port: 5200,
|
|
22
|
+
strictPort: true
|
|
23
|
+
},
|
|
24
|
+
root: config.isSsrBuild ? "source/server" : "source/client",
|
|
25
|
+
plugins: config.isSsrBuild ? [] : [react()],
|
|
26
|
+
ssr: {
|
|
27
|
+
noExternal: true,
|
|
28
|
+
external: externalDependencies
|
|
29
|
+
},
|
|
30
|
+
build: {
|
|
31
|
+
emptyOutDir: true,
|
|
32
|
+
rolldownOptions: {
|
|
33
|
+
input: config.isSsrBuild ? "main.ts" : "index.html"
|
|
34
|
+
},
|
|
35
|
+
outDir: resolve(import.meta.dirname, config.isSsrBuild ? "dist/server" : "dist/client"),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"compile": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
|
|
8
|
+
"build": "node --run compile && node dist/build-template.js",
|
|
8
9
|
"prepack": "node --run build"
|
|
9
10
|
},
|
|
10
11
|
"bin": {
|