@pipelex/create-method-app 0.4.0
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 +79 -0
- package/bin/create-method-app.mjs +17 -0
- package/lib/args.mjs +116 -0
- package/lib/destination.mjs +53 -0
- package/lib/git.mjs +205 -0
- package/lib/main.mjs +453 -0
- package/lib/make.mjs +105 -0
- package/lib/pack.mjs +110 -0
- package/lib/templates.mjs +60 -0
- package/lib/verdict.mjs +46 -0
- package/lib/write.mjs +134 -0
- package/package.json +41 -0
- package/templates/webapp-js.pack +0 -0
- package/templates.json +30 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table of templates this initializer serves, read from `templates.json`
|
|
3
|
+
* at the package's root, and the flags derived from it.
|
|
4
|
+
*
|
|
5
|
+
* The table is JSON rather than code so that the family's root can read every
|
|
6
|
+
* initializer's table whatever its language: a root test fails when a template
|
|
7
|
+
* of the family is served by no initializer or by more than one, and another
|
|
8
|
+
* holds `create` to the `make create` contract every template forwards.
|
|
9
|
+
*
|
|
10
|
+
* Each create variable is a flag named after it: `NAME` is `--name`,
|
|
11
|
+
* `AUTHOR_EMAIL` is `--author-email`, and a switch such as `DRY_RUN` is
|
|
12
|
+
* `--dry-run`, taking no value. Those are also the flags of a template's own
|
|
13
|
+
* `npm run create`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
/** The initializer package's root: the directory holding `package.json`. */
|
|
21
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
|
|
23
|
+
export const TABLE_FILE = path.join(PACKAGE_ROOT, "templates.json");
|
|
24
|
+
|
|
25
|
+
/** `AUTHOR_EMAIL` → `--author-email`. */
|
|
26
|
+
export function flagOf(variable) {
|
|
27
|
+
return `--${variable.toLowerCase().replaceAll("_", "-")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The table, with every flag it implies. */
|
|
31
|
+
export function loadTable(file = TABLE_FILE) {
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
33
|
+
const switches = new Set(raw.create.switches);
|
|
34
|
+
/** Every create variable any template takes, by its flag. */
|
|
35
|
+
const variables = new Map();
|
|
36
|
+
for (const variable of raw.create.shared) variables.set(flagOf(variable), variable);
|
|
37
|
+
for (const template of Object.values(raw.templates)) {
|
|
38
|
+
for (const variable of template.extras) variables.set(flagOf(variable), variable);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ecosystem: raw.ecosystem,
|
|
42
|
+
defaultTemplate: raw.default,
|
|
43
|
+
templates: raw.templates,
|
|
44
|
+
required: raw.create.required,
|
|
45
|
+
shared: raw.create.shared,
|
|
46
|
+
switches,
|
|
47
|
+
variables,
|
|
48
|
+
otherEcosystems: raw.otherEcosystems,
|
|
49
|
+
/** The variables one template's `make create` takes, in the order they are forwarded. */
|
|
50
|
+
variablesOf(template) {
|
|
51
|
+
return [...raw.create.shared, ...raw.templates[template].extras];
|
|
52
|
+
},
|
|
53
|
+
/** The command serving another ecosystem's template, or null. The suffix is the family's naming rule. */
|
|
54
|
+
otherEcosystemOf(template) {
|
|
55
|
+
const language = /-([a-z0-9]+)$/.exec(template)?.[1];
|
|
56
|
+
if (language === undefined || language === raw.ecosystem) return null;
|
|
57
|
+
return Object.hasOwn(raw.otherEcosystems, language) ? raw.otherEcosystems[language] : null;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/lib/verdict.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdicts. A run ends with one line whose first word is stable —
|
|
3
|
+
* `created`, `copied`, `refused: <why>` or `failed: <what>` — and that line is
|
|
4
|
+
* the last one printed, so a caller reads the verdict from the last line. The
|
|
5
|
+
* exit code is presentation: 0 for `created` and `copied`, 1 otherwise.
|
|
6
|
+
*
|
|
7
|
+
* created <dir> the copy, the git outcome, and a green make create
|
|
8
|
+
* copied <dir> --no-create or --dry-run: the copy and the git outcome only
|
|
9
|
+
* refused: <why> the preflight stopped, and nothing was written
|
|
10
|
+
* failed: write the copy failed or was interrupted, and what it created was removed
|
|
11
|
+
* failed: commit git refused the pristine commit; the copy stands
|
|
12
|
+
* failed: create make create did not succeed; the copy and the commit stand, and its
|
|
13
|
+
* own message says what to run next
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const EXIT_OK = 0;
|
|
17
|
+
export const EXIT_STOPPED = 1;
|
|
18
|
+
|
|
19
|
+
/** A run's end, thrown from wherever it is decided and printed once by the orchestration. */
|
|
20
|
+
export class Verdict extends Error {
|
|
21
|
+
constructor(word, detail, exitCode) {
|
|
22
|
+
super(`${word} — ${detail}`);
|
|
23
|
+
this.name = "Verdict";
|
|
24
|
+
this.word = word;
|
|
25
|
+
this.detail = detail;
|
|
26
|
+
this.exitCode = exitCode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static refused(why, detail) {
|
|
30
|
+
return new Verdict(`refused: ${why}`, detail, EXIT_STOPPED);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static failed(what, detail) {
|
|
34
|
+
return new Verdict(`failed: ${what}`, detail, EXIT_STOPPED);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The line printed last. */
|
|
38
|
+
get line() {
|
|
39
|
+
return this.message;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Quote a word for a line a person may paste into a shell. */
|
|
44
|
+
export function shellQuote(word) {
|
|
45
|
+
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
46
|
+
}
|
package/lib/write.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write, which removes exactly what it created when it cannot finish.
|
|
3
|
+
*
|
|
4
|
+
* Every directory is made without `recursive` and every file is opened with
|
|
5
|
+
* `wx`, so something that appears between the reading of the destination and
|
|
6
|
+
* the write makes the write fail rather than be written over or into. Each
|
|
7
|
+
* directory and file is recorded the moment it exists, and on a failure or an
|
|
8
|
+
* interruption the files go first, then the directories, deepest first and
|
|
9
|
+
* only when empty, the destination itself among them only when this write
|
|
10
|
+
* made it. Nothing the write did not create is ever removed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { MODES } from "./pack.mjs";
|
|
17
|
+
import { readDestination } from "./destination.mjs";
|
|
18
|
+
|
|
19
|
+
/** The write stopped because the run was interrupted. */
|
|
20
|
+
export class Interrupted extends Error {
|
|
21
|
+
constructor(signal) {
|
|
22
|
+
super(`${signal ?? "a signal"} interrupted the write`);
|
|
23
|
+
this.name = "Interrupted";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The destination changed between the preflight's reading and the write. */
|
|
28
|
+
export class DestinationChanged extends Error {
|
|
29
|
+
constructor(found) {
|
|
30
|
+
super("the destination changed after it was checked");
|
|
31
|
+
this.name = "DestinationChanged";
|
|
32
|
+
this.found = found;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the write created, in the order it created it. */
|
|
37
|
+
export class Created {
|
|
38
|
+
dirs = [];
|
|
39
|
+
files = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Make `dest` and each missing ancestor, recording each one made. */
|
|
43
|
+
async function makeDest(dest, created) {
|
|
44
|
+
const missing = [];
|
|
45
|
+
for (let at = dest; ; at = path.dirname(at)) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.stat(at);
|
|
48
|
+
break;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT") throw error;
|
|
51
|
+
}
|
|
52
|
+
missing.push(at);
|
|
53
|
+
if (path.dirname(at) === at) break;
|
|
54
|
+
}
|
|
55
|
+
for (const dir of missing.reverse()) {
|
|
56
|
+
await fs.mkdir(dir);
|
|
57
|
+
created.dirs.push(dir);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Make a directory under the destination with each parent it needs there,
|
|
63
|
+
* never reusing one this write did not make: a directory that appeared after
|
|
64
|
+
* the destination was read fails the `mkdir`.
|
|
65
|
+
*/
|
|
66
|
+
async function makeUnder(dir, dest, created, made) {
|
|
67
|
+
if (dir === dest || made.has(dir)) return;
|
|
68
|
+
await makeUnder(path.dirname(dir), dest, created, made);
|
|
69
|
+
await fs.mkdir(dir);
|
|
70
|
+
created.dirs.push(dir);
|
|
71
|
+
made.add(dir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Remove what was created: files, then directories deepest first, each only
|
|
76
|
+
* while empty. Returns the paths it could not remove, which a report names.
|
|
77
|
+
*/
|
|
78
|
+
export async function removeCreated(created) {
|
|
79
|
+
const left = [];
|
|
80
|
+
for (const file of [...created.files].reverse()) {
|
|
81
|
+
try {
|
|
82
|
+
await fs.unlink(file);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.code !== "ENOENT") left.push(file);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const depth = (dir) => dir.split(path.sep).length;
|
|
88
|
+
for (const dir of [...created.dirs].sort((a, b) => depth(b) - depth(a))) {
|
|
89
|
+
try {
|
|
90
|
+
await fs.rmdir(dir);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code !== "ENOENT") left.push(dir);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return left;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Write `files` (`[{ path, mode, data }]`, paths relative) under `dest`. The
|
|
100
|
+
* destination is made when missing, then read again; `signal` is checked
|
|
101
|
+
* before each file. On any failure what was created is removed, and the error
|
|
102
|
+
* is rethrown with `created` and `left` attached.
|
|
103
|
+
*/
|
|
104
|
+
export async function writeTree(dest, files, { signal, onFile } = {}) {
|
|
105
|
+
const created = new Created();
|
|
106
|
+
const made = new Set();
|
|
107
|
+
try {
|
|
108
|
+
await makeDest(dest, created);
|
|
109
|
+
const found = readDestination(dest);
|
|
110
|
+
if (found.kind !== "missing" && found.kind !== "empty" && found.kind !== "lone-git") {
|
|
111
|
+
throw new DestinationChanged(found);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [index, file] of files.entries()) {
|
|
115
|
+
if (signal?.aborted) throw new Interrupted(signal.reason);
|
|
116
|
+
const full = path.join(dest, ...file.path.split("/"));
|
|
117
|
+
await makeUnder(path.dirname(full), dest, created, made);
|
|
118
|
+
const handle = await fs.open(full, "wx", MODES[file.mode]);
|
|
119
|
+
created.files.push(full);
|
|
120
|
+
try {
|
|
121
|
+
await handle.writeFile(file.data);
|
|
122
|
+
} finally {
|
|
123
|
+
await handle.close();
|
|
124
|
+
}
|
|
125
|
+
await onFile?.(index, full);
|
|
126
|
+
}
|
|
127
|
+
if (signal?.aborted) throw new Interrupted(signal.reason);
|
|
128
|
+
return created;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
error.created = created;
|
|
131
|
+
error.left = await removeCreated(created);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pipelex/create-method-app",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Start a Pipelex method app: npm create @pipelex/method-app@latest my-app -- --method <bundle | mt_… | address>",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pipelex",
|
|
7
|
+
"mthds",
|
|
8
|
+
"create",
|
|
9
|
+
"template"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"create-method-app": "bin/create-method-app.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"lib/",
|
|
19
|
+
"templates/",
|
|
20
|
+
"templates.json"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.12.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Pipelex/pipelex-method-apps.git",
|
|
28
|
+
"directory": "initializers/js"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Pipelex/pipelex-method-apps/tree/main/initializers/js#readme",
|
|
31
|
+
"bugs": "https://github.com/Pipelex/pipelex-method-apps/issues",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"pack-templates": "node scripts/pack-templates.mjs",
|
|
38
|
+
"prepack": "node scripts/pack-templates.mjs --publish",
|
|
39
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
40
|
+
}
|
|
41
|
+
}
|
|
Binary file
|
package/templates.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ecosystem": "js",
|
|
3
|
+
"default": "webapp-js",
|
|
4
|
+
"templates": {
|
|
5
|
+
"webapp-js": {
|
|
6
|
+
"extras": ["METHOD_NAME", "LABEL"]
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"create": {
|
|
10
|
+
"required": "METHOD",
|
|
11
|
+
"shared": [
|
|
12
|
+
"METHOD",
|
|
13
|
+
"NAME",
|
|
14
|
+
"TITLE",
|
|
15
|
+
"DESCRIPTION",
|
|
16
|
+
"PIPE",
|
|
17
|
+
"AUTHOR_NAME",
|
|
18
|
+
"AUTHOR_EMAIL",
|
|
19
|
+
"REPO_URL",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"LICENSE_HOLDER",
|
|
22
|
+
"LICENSE_YEAR",
|
|
23
|
+
"DRY_RUN"
|
|
24
|
+
],
|
|
25
|
+
"switches": ["DRY_RUN"]
|
|
26
|
+
},
|
|
27
|
+
"otherEcosystems": {
|
|
28
|
+
"python": "uvx create-pipelex-method-app"
|
|
29
|
+
}
|
|
30
|
+
}
|