@pajh/buldng 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buldng-lib.js +339 -125
- package/dist/buldng-ops.js +258 -0
- package/dist/buldng-validate.js +114 -9
- package/dist/buldng.css +148 -0
- package/dist/clean.sh +14 -0
- package/dist/layout.ts +184 -0
- package/dist/types/global.d.ts +10 -0
- package/dist/workfile.js +37 -130
- package/dist/wrapper.html +24 -0
- package/package.json +2 -2
package/dist/buldng-lib.js
CHANGED
|
@@ -3,32 +3,146 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import esbuild from "esbuild";
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { readFileSync } from "node:fs";
|
|
9
10
|
|
|
10
11
|
import { WorkFile } from "./workfile.js";
|
|
11
|
-
import { getDest, assertDestWrite, assertSourceRead, getInputs
|
|
12
|
-
|
|
12
|
+
import { getDest, setSource, setDest, assertDestWrite, assertSourceRead, getInputs, looksLikeFilename, looksLikeHTML,
|
|
13
|
+
looksLikeCSS, looksLikeJS, ensureLeadingSlash, ensureWorkFile } from "./buldng-validate.js";
|
|
14
|
+
import { file, literal, wrap, append, replace, compile, materialise}
|
|
15
|
+
from "./buldng-ops.js";
|
|
16
|
+
export { file, literal, wrap, append, replace, compile, materialise }
|
|
17
|
+
from "./buldng-ops.js";
|
|
18
|
+
export { assertSourceRead, assertDestWrite, setSource, setDest, looksLikeFilename,
|
|
19
|
+
looksLikeHTML, looksLikeCSS, looksLikeJS, ensureWorkFile, ensureLeadingSlash } from "./buldng-validate.js";
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
setSource,
|
|
23
|
+
setDest,
|
|
24
|
+
mkdir,
|
|
25
|
+
deepCopy,
|
|
26
|
+
file,
|
|
27
|
+
literal,
|
|
28
|
+
wrap,
|
|
29
|
+
append,
|
|
30
|
+
replace,
|
|
31
|
+
materialise,
|
|
32
|
+
compile,
|
|
33
|
+
csslink,
|
|
34
|
+
scriptlink,
|
|
35
|
+
pageHTML,
|
|
36
|
+
buldngHTMLWrapper,
|
|
37
|
+
reg,
|
|
38
|
+
printHeader
|
|
39
|
+
};
|
|
13
40
|
|
|
14
41
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
42
|
const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
|
|
43
|
+
const registry = new Map();
|
|
44
|
+
|
|
45
|
+
export function reg(key, value) {
|
|
46
|
+
if (typeof key !== "string") {
|
|
47
|
+
throw new Error(`reg(): key must be a string`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const wf = ensureWorkFile(value, "reg");
|
|
51
|
+
|
|
52
|
+
if (registry.has(key)) {
|
|
53
|
+
console.log(`reg(): overwriting existing key '${key}'`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
registry.set(key, wf);
|
|
57
|
+
return wf;
|
|
58
|
+
}
|
|
16
59
|
|
|
17
60
|
let headerPrinted = false;
|
|
18
61
|
export function printHeader(importMeta) {
|
|
19
62
|
if (headerPrinted) return;
|
|
20
63
|
headerPrinted = true;
|
|
21
|
-
|
|
64
|
+
|
|
22
65
|
const version = pkg.version;
|
|
23
66
|
const timestamp = new Date().toLocaleString("en-IE", { hour12: false });
|
|
24
|
-
|
|
67
|
+
|
|
25
68
|
const callerFile = fileURLToPath(importMeta.url);
|
|
26
69
|
const callerDir = dirname(callerFile);
|
|
27
|
-
|
|
28
|
-
console.log(
|
|
70
|
+
|
|
71
|
+
console.log(`*local* buldng build system v${version} — ${timestamp}`);
|
|
29
72
|
console.log(`${callerDir}/${callerFile.split("/").pop()}`);
|
|
30
73
|
}
|
|
31
74
|
|
|
75
|
+
export function csslink(input) {
|
|
76
|
+
const pre = `<link rel="stylesheet" href="`;
|
|
77
|
+
const post = `">`;
|
|
78
|
+
|
|
79
|
+
// --------------------------------------------------
|
|
80
|
+
// Case 1: string
|
|
81
|
+
// --------------------------------------------------
|
|
82
|
+
if (typeof input === "string") {
|
|
83
|
+
if (!input.endsWith(".css")) {
|
|
84
|
+
throw new Error(`csslink(): expected a .css filename, got '${input}'`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new WorkFile(
|
|
88
|
+
"literal",
|
|
89
|
+
{ value: pre + ensureLeadingSlash(input) + post },
|
|
90
|
+
[],
|
|
91
|
+
"csslink"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --------------------------------------------------
|
|
96
|
+
// Case 2: WorkFile
|
|
97
|
+
// --------------------------------------------------
|
|
98
|
+
if (input instanceof WorkFile) {
|
|
99
|
+
return new WorkFile(
|
|
100
|
+
"wrap",
|
|
101
|
+
{ pre, post },
|
|
102
|
+
[input],
|
|
103
|
+
"csslink"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error(`csslink(): expected string or WorkFile, got ${typeof input}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function scriptlink(input) {
|
|
111
|
+
const pre = `<script type="module" src="/`;
|
|
112
|
+
const post = `"></script>`;
|
|
113
|
+
|
|
114
|
+
// --------------------------------------------------
|
|
115
|
+
// Case 1: string
|
|
116
|
+
// --------------------------------------------------
|
|
117
|
+
if (typeof input === "string") {
|
|
118
|
+
if (!input.endsWith(".js")) {
|
|
119
|
+
throw new Error(`scriptlink(): expected a .js filename, got '${input}'`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new WorkFile(
|
|
123
|
+
"literal",
|
|
124
|
+
{ value: pre + input + post },
|
|
125
|
+
[],
|
|
126
|
+
"scriptlink"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --------------------------------------------------
|
|
131
|
+
// Case 2: WorkFile
|
|
132
|
+
// --------------------------------------------------
|
|
133
|
+
if (input instanceof WorkFile) {
|
|
134
|
+
return new WorkFile(
|
|
135
|
+
"wrap",
|
|
136
|
+
{ pre, post },
|
|
137
|
+
[input],
|
|
138
|
+
"scriptlink"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(`scriptlink(): expected string or WorkFile, got ${typeof input}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
32
146
|
// --------------------------------------------------
|
|
33
147
|
// mkdir("puzzle")
|
|
34
148
|
// --------------------------------------------------
|
|
@@ -42,153 +156,253 @@ export function mkdir(relDir) {
|
|
|
42
156
|
// --------------------------------------------------
|
|
43
157
|
export function deepCopy(from, to) {
|
|
44
158
|
let DEST = getDest();
|
|
45
|
-
|
|
159
|
+
|
|
46
160
|
console.log(`deepCopy: ${from} → ${to}`);
|
|
47
|
-
|
|
161
|
+
|
|
48
162
|
const absFrom = path.resolve(from);
|
|
49
|
-
|
|
163
|
+
|
|
50
164
|
if (to.includes("..")) {
|
|
51
165
|
throw new Error("SECURITY: Illegal path traversal in deepCopy()");
|
|
52
166
|
}
|
|
53
|
-
|
|
167
|
+
|
|
54
168
|
const absTo = to === "/" ? DEST : path.resolve(DEST, to);
|
|
55
|
-
|
|
169
|
+
|
|
56
170
|
if (!absTo.startsWith(DEST)) {
|
|
57
171
|
throw new Error(`SECURITY: deepCopy target escapes dest:
|
|
58
172
|
dest: ${DEST}
|
|
59
173
|
target: ${absTo}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!fs.existsSync(absFrom)) {
|
|
177
|
+
throw new Error(`deepCopy: source does not exist: ${absFrom}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function copyRecursive(src, dst) {
|
|
181
|
+
const stat = fs.statSync(src);
|
|
182
|
+
|
|
183
|
+
if (stat.isDirectory()) {
|
|
184
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
185
|
+
const entries = fs.readdirSync(src);
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
copyRecursive(path.join(src, entry), path.join(dst, entry));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
191
|
+
fs.copyFileSync(src, dst);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
copyRecursive(absFrom, absTo);
|
|
60
196
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
197
|
+
|
|
198
|
+
export function header() {
|
|
199
|
+
printHeader(import.meta);
|
|
64
200
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
function packageFileAbs(filename) {
|
|
204
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
205
|
+
const __dirname = dirname(__filename);
|
|
206
|
+
return join(__dirname, filename);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// wraps whatever you provide in the overall buldng html wrapper.
|
|
210
|
+
// also compiles buldng's layout.ts and saves layout.js to the site root.
|
|
211
|
+
export function buldngHTMLWrapper(source) {
|
|
212
|
+
const srcWF = ensureWorkFile(source, "source");
|
|
213
|
+
|
|
214
|
+
// 1. Compile buldng's layout.ts (lazy)
|
|
215
|
+
const layoutTS = new WorkFile("file", { absPath: packageFileAbs("layout.ts") }, []);
|
|
216
|
+
const layoutJS = compile(layoutTS);
|
|
217
|
+
|
|
218
|
+
// 2. Save compiled layout.js to the site root (this triggers execution)
|
|
219
|
+
layoutJS.save("layout.js");
|
|
220
|
+
|
|
221
|
+
// 2.5 Save camera.css to the site root (this triggers execution)
|
|
222
|
+
const buldngCSS = new WorkFile("file", { absPath: packageFileAbs("buldng.css") }, []);
|
|
223
|
+
buldngCSS.save("buldng.css");
|
|
224
|
+
|
|
225
|
+
// 3. Wrap project shell in buldng's wrapper.html
|
|
226
|
+
const wrapperWF = new WorkFile("file", { absPath: packageFileAbs("wrapper.html") }, []);
|
|
227
|
+
return replace(wrapperWF, "<!--CAMERA-STYLE-->", srcWF);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
const _warned = {
|
|
233
|
+
shell: false,
|
|
234
|
+
projectCSS: false
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export function pageHTML({ app, css = null, js = null, out }) {
|
|
238
|
+
if (!out || typeof out !== "string") {
|
|
239
|
+
throw new Error("pageHTML(): 'out' must be a non-empty string");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!app) {
|
|
243
|
+
throw new Error("pageHTML(): 'app' is required and cannot be null");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const appWF = ensureWorkFile(app, "app");
|
|
247
|
+
|
|
248
|
+
// Look up optional registered assets
|
|
249
|
+
const shellWF = registry.get("shell") ?? null;
|
|
250
|
+
const projectCSS = registry.get("projectCSS") ?? null;
|
|
251
|
+
|
|
252
|
+
// One-time warnings
|
|
253
|
+
if (!shellWF && !_warned.shell) {
|
|
254
|
+
console.warn(
|
|
255
|
+
"pageHTML(): no 'shell' registered — building raw page HTML.\n" +
|
|
256
|
+
"To register a shell use: reg('shell', file`layout/shell.html`)"
|
|
257
|
+
);
|
|
258
|
+
_warned.shell = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!projectCSS && !_warned.projectCSS) {
|
|
262
|
+
console.warn(
|
|
263
|
+
"pageHTML(): no 'projectCSS' registered — page will not include project CSS.\n" +
|
|
264
|
+
"To register project CSS use: reg('projectCSS', file`layout/buldng.css`)"
|
|
265
|
+
);
|
|
266
|
+
_warned.projectCSS = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --------------------------------------------------
|
|
270
|
+
// Build HTML WorkFile lazily
|
|
271
|
+
// --------------------------------------------------
|
|
272
|
+
|
|
273
|
+
let htmlWF;
|
|
274
|
+
|
|
275
|
+
if (shellWF) {
|
|
276
|
+
htmlWF = replace(shellWF, "<!--APP-->", appWF);
|
|
75
277
|
} else {
|
|
76
|
-
|
|
77
|
-
|
|
278
|
+
htmlWF = appWF;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Insert JS lazily
|
|
282
|
+
if (js) {
|
|
283
|
+
const jsWF = resolveAsset(js, {
|
|
284
|
+
ext: ".js",
|
|
285
|
+
tag: "scriptlink",
|
|
286
|
+
wrap: scriptlink,
|
|
287
|
+
autoPrefix: "js"
|
|
288
|
+
});
|
|
289
|
+
htmlWF = replace(htmlWF, "<!--SCRIPTS-->", jsWF);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Insert CSS lazily (project first)
|
|
293
|
+
const cssWFs = [];
|
|
294
|
+
|
|
295
|
+
if (projectCSS) {
|
|
296
|
+
cssWFs.push(resolveAsset(projectCSS, {
|
|
297
|
+
ext: ".css",
|
|
298
|
+
tag: "csslink",
|
|
299
|
+
wrap: csslink,
|
|
300
|
+
autoPrefix: "css"
|
|
301
|
+
}));
|
|
302
|
+
console.log(`${out}: has projectCSS`);
|
|
78
303
|
}
|
|
304
|
+
|
|
305
|
+
if (css) {
|
|
306
|
+
cssWFs.push(resolveAsset(css, {
|
|
307
|
+
ext: ".css",
|
|
308
|
+
tag: "csslink",
|
|
309
|
+
wrap: csslink,
|
|
310
|
+
autoPrefix: "css"
|
|
311
|
+
}));
|
|
312
|
+
console.log(`${out}: has pageCSS`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (cssWFs.length === 0) {
|
|
316
|
+
// No CSS at all → do nothing
|
|
317
|
+
// (This is allowed)
|
|
318
|
+
} else if (cssWFs.length === 1) {
|
|
319
|
+
// One CSS → inject directly
|
|
320
|
+
htmlWF = replace(htmlWF, "<!--CSS-->", cssWFs[0]);
|
|
321
|
+
} else {
|
|
322
|
+
// Two CSS → append them
|
|
323
|
+
const cssListWF = append(...cssWFs);
|
|
324
|
+
htmlWF = replace(htmlWF, "<!--CSS-->", cssListWF);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The ONLY public collapse operator
|
|
328
|
+
return htmlWF.save(out);
|
|
79
329
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
330
|
+
|
|
331
|
+
let autoCounter = 1;
|
|
332
|
+
|
|
333
|
+
function resolveAsset(input, {
|
|
334
|
+
ext, // ".css" or ".js"
|
|
335
|
+
tag, // "csslink" or "jslink"
|
|
336
|
+
wrap, // csslink() or stylelink()
|
|
337
|
+
autoPrefix // "css" or "js"
|
|
338
|
+
}) {
|
|
339
|
+
if (input == null) return null;
|
|
340
|
+
|
|
341
|
+
// Case 1: string
|
|
342
|
+
if (typeof input === "string") {
|
|
343
|
+
if (!input.endsWith(ext)) {
|
|
344
|
+
throw new Error(`pageHTML(${ext}): expected a ${ext} filename, got '${input}'`);
|
|
345
|
+
}
|
|
346
|
+
return wrap(input);
|
|
94
347
|
}
|
|
95
348
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return new WorkFile("file", { relPath, absPath }, []);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// --------------------------------------------------
|
|
103
|
-
// literal(text)
|
|
104
|
-
// --------------------------------------------------
|
|
105
|
-
export function literal(value) {
|
|
106
|
-
return new WorkFile("literal", { value }, []);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// --------------------------------------------------
|
|
110
|
-
// ensureWorkFile(x)
|
|
111
|
-
// --------------------------------------------------
|
|
112
|
-
function ensureWorkFile(x, fieldName = "value") {
|
|
113
|
-
if (x instanceof WorkFile) return x;
|
|
114
|
-
|
|
115
|
-
if (typeof x === "string") {
|
|
116
|
-
return literal(x);
|
|
349
|
+
// Case 2: must be WorkFile
|
|
350
|
+
if (!(input instanceof WorkFile)) {
|
|
351
|
+
throw new Error(`pageHTML(${ext}): expected string or WorkFile, got ${typeof input}`);
|
|
117
352
|
}
|
|
118
353
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// csslink(filename)
|
|
124
|
-
// --------------------------------------------------
|
|
125
|
-
export function csslink(filename) {
|
|
126
|
-
return new WorkFile(
|
|
127
|
-
"literal",
|
|
128
|
-
{ value: `<link rel="stylesheet" href="/${filename}">` },
|
|
129
|
-
[]
|
|
130
|
-
);
|
|
131
|
-
}
|
|
354
|
+
// Case 3: already a link
|
|
355
|
+
if (input.tag === tag) {
|
|
356
|
+
return input;
|
|
357
|
+
}
|
|
132
358
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
export function scriptlink(filename) {
|
|
137
|
-
return new WorkFile(
|
|
138
|
-
"literal",
|
|
139
|
-
{ value: `<script type="module" src="/${filename}"></script>` },
|
|
140
|
-
[]
|
|
141
|
-
);
|
|
142
|
-
}
|
|
359
|
+
// Case 4: literal WorkFile
|
|
360
|
+
if (input.op === "literal") {
|
|
361
|
+
const value = input.config.value;
|
|
143
362
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
export function append(...items) {
|
|
148
|
-
const children = items.map(item => {
|
|
149
|
-
if (item instanceof WorkFile) return item;
|
|
150
|
-
if (typeof item === "string") return literal(item);
|
|
151
|
-
throw new Error("append(): each item must be WorkFile or string");
|
|
152
|
-
});
|
|
363
|
+
if (value.endsWith(ext)) {
|
|
364
|
+
return wrap(input);
|
|
365
|
+
}
|
|
153
366
|
|
|
154
|
-
|
|
155
|
-
|
|
367
|
+
throw new Error(
|
|
368
|
+
`pageHTML(${ext}): literal WorkFile does not end in ${ext} → '${value}'`
|
|
369
|
+
);
|
|
156
370
|
}
|
|
157
371
|
|
|
158
|
-
|
|
159
|
-
|
|
372
|
+
// Case 5a: materialise WorkFile
|
|
373
|
+
if (input.op === "materialise") {
|
|
374
|
+
const target = input.config.target;
|
|
160
375
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const tagWF = ensureWorkFile(tag, "tag");
|
|
167
|
-
const repWF = ensureWorkFile(replacement, "replacement");
|
|
376
|
+
if (!target.endsWith(ext)) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`pageHTML(${ext}): materialise target '${target}' does not end in ${ext}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
168
381
|
|
|
169
|
-
|
|
170
|
-
throw new Error(`replace(): tag cannot be blank`);
|
|
382
|
+
return wrap(input);
|
|
171
383
|
}
|
|
172
384
|
|
|
173
|
-
|
|
174
|
-
}
|
|
385
|
+
// Case 5b: blob → auto-materialise
|
|
386
|
+
const autoName = `${autoPrefix}${autoCounter++}${ext}`;
|
|
175
387
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
return
|
|
388
|
+
console.warn(
|
|
389
|
+
`pageHTML(${ext}): received non-literal WorkFile; auto-materialising as ${autoName}`
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const mat = materialise(input, autoName);
|
|
393
|
+
return wrap(mat);
|
|
182
394
|
}
|
|
183
395
|
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
396
|
+
|
|
397
|
+
// --------------------------------------------------
|
|
398
|
+
// build-inputs.json manifest
|
|
399
|
+
// --------------------------------------------------
|
|
400
|
+
process.on("exit", () => {
|
|
401
|
+
if (process.env.BULDNG_HOT !== "1") return;
|
|
402
|
+
let DEST = getDest();
|
|
403
|
+
let INPUTS = getInputs();
|
|
404
|
+
const manifest = Array.from(INPUTS);
|
|
405
|
+
const out = path.join(DEST, "build-inputs.json");
|
|
406
|
+
fs.writeFileSync(out, JSON.stringify(manifest, null, 2));
|
|
407
|
+
});
|
|
408
|
+
|