saladplate 0.1.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 +83 -0
- package/dist/bin/simplate.d.ts +2 -0
- package/dist/bin/simplate.js +156 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +75 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 - present Zach Snow
|
|
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,83 @@
|
|
|
1
|
+
# Saladplate
|
|
2
|
+
|
|
3
|
+
_Simple templating; it's smaller than usual._
|
|
4
|
+
|
|
5
|
+
Saladplate is a very simple templating tool, with 3 features:
|
|
6
|
+
|
|
7
|
+
1. Replace `${{ VAR }}` with the contents of the environment variable `VAR`.
|
|
8
|
+
2. Replace `$<< filename >>` with the contents of the file `filename`.
|
|
9
|
+
3. Replace `$(( some command ))` with the output of the command `some command`.
|
|
10
|
+
|
|
11
|
+
If you need more functionality that that, this is not the templating tool for you.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ npm -g install saladplate
|
|
17
|
+
$ saladplate --version
|
|
18
|
+
saladplate: version 0.1.0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The easiest way to use Saladplate with via the `saladplate` command line tool:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
$ saladplate --help
|
|
27
|
+
|
|
28
|
+
Usage: saladplate [options] <file>...
|
|
29
|
+
Options:
|
|
30
|
+
--debug enable debug mode
|
|
31
|
+
-v, --version show version information
|
|
32
|
+
-h, --help show this help message
|
|
33
|
+
-d, --directory output directory
|
|
34
|
+
-s, --suffix output suffix; only applies when using --directory
|
|
35
|
+
-o, --output output file; overrides --directory and --suffix
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Saladplate generally operates in 2 modes. If you pass `--directory`, then
|
|
39
|
+
each file you pass will be templated independently, and the output written
|
|
40
|
+
into the given directory. Use `--suffix` to change the extension of the output
|
|
41
|
+
file -- for instance, if you template the files `some.template` and `another.txt`
|
|
42
|
+
with `--directory=out` and `--suffix=.html` you will end up with a file `out/some.html`
|
|
43
|
+
and `out/another.html`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ saladplate --directory=out --suffix=.html *.template
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If instead you pass `--output`, then each file will be templated, with the
|
|
50
|
+
results _concatenated_ into the single given output file.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$ saladplate --output=index.html *.template
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can also use the exported `template` function to template a string:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { template } from "saladplate";
|
|
60
|
+
console.info(template("The current PATH is: $PATH"));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
When developing locally, you can test your changes using the *script* `saladplate`,
|
|
66
|
+
which uses `ts-node`. Note the use of `run` below, as well as the use of `--` to
|
|
67
|
+
separate arguments to `npm run` from arguments to `saladplate`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
$ npm run saladplate -- --version
|
|
71
|
+
|
|
72
|
+
> saladplate@0.1.0 saladplate
|
|
73
|
+
> ts-node ./bin/saladplate.ts
|
|
74
|
+
|
|
75
|
+
saladplate: version 0.1.0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Tests are located in `tests/` and consist of input templates `tests/*.test` and
|
|
79
|
+
corresponding expected outputs `tests/*.expected`. To run tests:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ npm run test
|
|
83
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import process from "process";
|
|
14
|
+
import util from "util";
|
|
15
|
+
import { BIN, template } from "../src/index";
|
|
16
|
+
const parseArgsConfig = {
|
|
17
|
+
// Exclude the first two arguments: node and the script itself.
|
|
18
|
+
args: process.argv.slice(2),
|
|
19
|
+
options: {
|
|
20
|
+
debug: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
default: false,
|
|
23
|
+
description: "enable debug mode",
|
|
24
|
+
},
|
|
25
|
+
version: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
default: false,
|
|
28
|
+
short: "v",
|
|
29
|
+
description: "show version information",
|
|
30
|
+
},
|
|
31
|
+
help: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
default: false,
|
|
34
|
+
short: "h",
|
|
35
|
+
description: "show this help message",
|
|
36
|
+
},
|
|
37
|
+
directory: {
|
|
38
|
+
type: "string",
|
|
39
|
+
short: "d",
|
|
40
|
+
description: "output directory",
|
|
41
|
+
},
|
|
42
|
+
suffix: {
|
|
43
|
+
type: "string",
|
|
44
|
+
short: "s",
|
|
45
|
+
description: "output suffix; only applies when using --directory",
|
|
46
|
+
},
|
|
47
|
+
output: {
|
|
48
|
+
type: "string",
|
|
49
|
+
short: "o",
|
|
50
|
+
description: "output file; overrides --directory and --suffix",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
allowPositionals: true,
|
|
54
|
+
};
|
|
55
|
+
function help() {
|
|
56
|
+
console.info(`Usage: ${BIN} [options] <file>...`);
|
|
57
|
+
console.info(`Options:`);
|
|
58
|
+
const entries = Object.entries(parseArgsConfig.options);
|
|
59
|
+
const prefixes = {};
|
|
60
|
+
entries.forEach(([name, option]) => {
|
|
61
|
+
const short = "short" in option ? `-${option.short}, ` : "";
|
|
62
|
+
const prefix = ` ${short}--${name}`;
|
|
63
|
+
prefixes[name] = prefix;
|
|
64
|
+
});
|
|
65
|
+
const len = Math.max(...entries.map(([name, option]) => prefixes[name].length)) + 2;
|
|
66
|
+
entries.forEach(([name, option]) => {
|
|
67
|
+
var _a;
|
|
68
|
+
const prefix = prefixes[name];
|
|
69
|
+
console.info(`${prefix}${" ".repeat(len - prefix.length)}${(_a = option.description) !== null && _a !== void 0 ? _a : ""}`);
|
|
70
|
+
});
|
|
71
|
+
console.info("");
|
|
72
|
+
}
|
|
73
|
+
function version() {
|
|
74
|
+
const version = process.env.npm_package_version;
|
|
75
|
+
console.info(`${BIN}: version ${version !== null && version !== void 0 ? version : "unknown"}`);
|
|
76
|
+
}
|
|
77
|
+
const parseArgs = () => {
|
|
78
|
+
const { values: options, positionals: filenames } = util.parseArgs(parseArgsConfig);
|
|
79
|
+
return { options, filenames };
|
|
80
|
+
};
|
|
81
|
+
let output = null;
|
|
82
|
+
function outputForFilename(filename, options) {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
if (output) {
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
if (options.output) {
|
|
88
|
+
output = {
|
|
89
|
+
file: yield fs.promises.open(options.output, "w"),
|
|
90
|
+
filename: options.output,
|
|
91
|
+
};
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
if (options.directory) {
|
|
95
|
+
const basename = path.basename(filename);
|
|
96
|
+
let outputFilename = path.join(options.directory, basename);
|
|
97
|
+
if (options.suffix) {
|
|
98
|
+
outputFilename = outputFilename.replace(/\.[^.]+$/, options.suffix);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
file: outputFilename,
|
|
102
|
+
filename: outputFilename,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
output = {
|
|
106
|
+
file: yield fs.promises.open("/dev/stdout", "w"),
|
|
107
|
+
filename: "/dev/stdout",
|
|
108
|
+
};
|
|
109
|
+
return output;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function main() {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const { options, filenames } = parseArgs();
|
|
115
|
+
if (options.debug) {
|
|
116
|
+
console.debug(`${BIN}: debug mode enabled`);
|
|
117
|
+
}
|
|
118
|
+
if (options.help) {
|
|
119
|
+
help();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (options.version) {
|
|
123
|
+
version();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (filenames.length === 0) {
|
|
127
|
+
console.error(`${BIN}: no input files; use -- for stdin`);
|
|
128
|
+
help();
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
yield Promise.all(filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
|
|
132
|
+
if (filename === "--") {
|
|
133
|
+
filename = "/dev/stdin";
|
|
134
|
+
}
|
|
135
|
+
options.debug && console.debug(`${BIN}: reading ${filename}...`);
|
|
136
|
+
const content = yield fs.promises.readFile(filename, {
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
});
|
|
139
|
+
options.debug && console.debug(`${BIN}: templating ${filename}...`);
|
|
140
|
+
const output = yield template(content, filename, options);
|
|
141
|
+
const { file: outputFile, filename: outputFilename } = yield outputForFilename(filename, options);
|
|
142
|
+
options.debug && console.debug(`${BIN}: writing ${outputFilename}...`);
|
|
143
|
+
yield fs.promises.writeFile(outputFile, output, {
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
});
|
|
146
|
+
})));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
main()
|
|
150
|
+
.then((i) => {
|
|
151
|
+
process.exit(i !== null && i !== void 0 ? i : 0);
|
|
152
|
+
})
|
|
153
|
+
.catch((e) => {
|
|
154
|
+
console.error(`${BIN}: unhandled error:`, e);
|
|
155
|
+
process.exit(-1);
|
|
156
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The name of the tool; prefixes most output.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BIN = "simplate";
|
|
5
|
+
/**
|
|
6
|
+
* Templating options for `template`.
|
|
7
|
+
*/
|
|
8
|
+
export type Options = {
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Replace variables, file includes, and command executions in the given `content`, which
|
|
13
|
+
* is assumed to have been read from the given `filename` (or stdin). Collapses newlines
|
|
14
|
+
* at the end of the output so that there's just one.
|
|
15
|
+
*/
|
|
16
|
+
export declare const template: (content: string, filename?: string, options?: Options) => Promise<string>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { exec } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import process from "process";
|
|
14
|
+
import util from "util";
|
|
15
|
+
/**
|
|
16
|
+
* The name of the tool; prefixes most output.
|
|
17
|
+
*/
|
|
18
|
+
export const BIN = "simplate";
|
|
19
|
+
const cwdForFilename = (filename) => {
|
|
20
|
+
return filename === "/dev/stdin" ? process.cwd() : path.dirname(filename);
|
|
21
|
+
};
|
|
22
|
+
const VAR_RE = /\$\{\{([^\}]+)\}\}/g;
|
|
23
|
+
const FILE_RE = /\$\<\<([^\>]+)\>\>/g;
|
|
24
|
+
const EXEC_RE = /\$\(\(([^\)]+)\)\)/g;
|
|
25
|
+
function replaceAsync(input, regexp, fn) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const replacements = yield Promise.all(Array.from(input.matchAll(regexp), (match) => fn(match)));
|
|
28
|
+
let i = 0;
|
|
29
|
+
return input.replace(regexp, () => replacements[i++]);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const replaceVar = (variable, filename, options) => {
|
|
33
|
+
var _a;
|
|
34
|
+
options.debug && console.debug(`${BIN}: ${filename}: evaluating ${variable} for replacement...`);
|
|
35
|
+
return (_a = process.env[variable]) !== null && _a !== void 0 ? _a : "";
|
|
36
|
+
};
|
|
37
|
+
const replaceFile = (includeFilename, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
+
// Resolve the include filename relative to the current file; if the current file is stdin
|
|
39
|
+
// then keep the current working directory.
|
|
40
|
+
const cwd = cwdForFilename(filename);
|
|
41
|
+
const include = path.join(cwd, includeFilename);
|
|
42
|
+
options.debug && console.debug(`${BIN}: ${filename}: reading ${include} for replacement...`);
|
|
43
|
+
const content = yield fs.promises.readFile(include, { encoding: "utf-8" });
|
|
44
|
+
return template(content, include, options);
|
|
45
|
+
});
|
|
46
|
+
const replaceExec = (command, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
// Execute the command in the working directory containing the file we are processing; if
|
|
48
|
+
// the current file is stdin then keep the current working directory.
|
|
49
|
+
const cwd = cwdForFilename(filename);
|
|
50
|
+
options.debug && console.debug(`${BIN}: ${filename}: executing ${command} for replacement...`);
|
|
51
|
+
const { stdout } = yield util.promisify(exec)(command, {
|
|
52
|
+
cwd,
|
|
53
|
+
});
|
|
54
|
+
return stdout;
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Replace variables, file includes, and command executions in the given `content`, which
|
|
58
|
+
* is assumed to have been read from the given `filename` (or stdin). Collapses newlines
|
|
59
|
+
* at the end of the output so that there's just one.
|
|
60
|
+
*/
|
|
61
|
+
export const template = (content, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
filename !== null && filename !== void 0 ? filename : (filename = "/dev/stdin");
|
|
63
|
+
options !== null && options !== void 0 ? options : (options = {});
|
|
64
|
+
content = yield replaceAsync(content, VAR_RE, (match) => {
|
|
65
|
+
return replaceVar(match[1].trim(), filename, options);
|
|
66
|
+
});
|
|
67
|
+
content = yield replaceAsync(content, FILE_RE, (match) => {
|
|
68
|
+
return replaceFile(match[1].trim(), filename, options);
|
|
69
|
+
});
|
|
70
|
+
content = yield replaceAsync(content, EXEC_RE, (match) => {
|
|
71
|
+
return replaceExec(match[1].trim(), filename, options);
|
|
72
|
+
});
|
|
73
|
+
content = content.replace(/\n+$/m, "\n");
|
|
74
|
+
return content;
|
|
75
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saladplate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Very simple templating.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"template",
|
|
7
|
+
"templating"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/src/saladplate.js",
|
|
10
|
+
"bin": {
|
|
11
|
+
"saladplate": "dist/bin/saladplate.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"types": "tsc --noEmit",
|
|
15
|
+
"build": "./build.sh",
|
|
16
|
+
"saladplate": "ts-node ./bin/saladplate.ts",
|
|
17
|
+
"test": "./test.sh"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.12.12",
|
|
21
|
+
"ts-node": "^10.9.2",
|
|
22
|
+
"typescript": "^5.4.5"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**/*"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/zachsnow/saladplate.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/zachsnow/saladplate/issues"
|
|
34
|
+
}
|
|
35
|
+
}
|