create-monolayer-app 0.2.2 → 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/package.json +2 -2
- package/template/_gitignore +9 -4
- package/template/app/routes/files.ts +63 -0
- package/template/app/routes.ts +5 -2
- package/template/oxfmt.config.ts +6 -1
- package/template/oxlint.config.ts +3 -8
- package/template/package.json +2 -1
- package/template/vendor/file-bucket/client.ts +196 -0
- package/template/vendor/file-bucket/errors.ts +26 -0
- package/template/vendor/file-bucket/index.ts +7 -0
- package/template/vendor/file-bucket/multipart.ts +304 -0
- package/template/vendor/file-bucket/protocol.ts +222 -0
- package/template/vendor/file-bucket/react-router.ts +222 -0
- package/template/vendor/file-bucket/server.ts +544 -0
- package/template/vendor/file-bucket/transfers.ts +23 -0
- package/template/vendor/file-bucket/transport.ts +37 -0
- package/template/vendor/file-bucket/types.ts +153 -0
- package/template/vendor/file-bucket/vite.d.ts +7 -0
- package/template/vendor/file-bucket/vite.js +2482 -0
- package/template/vendor/file-bucket/xml.ts +325 -0
- package/template/vendor/package-app/package-app.mjs +116 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency XML parser, DOM builder, and serializer for S3 XML payloads.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// oxlint-disable-next-line max-classes-per-file
|
|
6
|
+
export const S3_NS = "http://s3.amazonaws.com/doc/2006-03-01/";
|
|
7
|
+
export const XML_DECL = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
8
|
+
|
|
9
|
+
export class ParseError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ParseError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function escapeXml(
|
|
17
|
+
str: string | number | boolean | null | undefined
|
|
18
|
+
): string {
|
|
19
|
+
if (str === null || str === undefined) return "";
|
|
20
|
+
return String(str)
|
|
21
|
+
.replaceAll("&", "&")
|
|
22
|
+
.replaceAll("<", "<")
|
|
23
|
+
.replaceAll(">", ">")
|
|
24
|
+
.replaceAll('"', """)
|
|
25
|
+
.replaceAll("'", "'");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function unescapeXml(str: string): string {
|
|
29
|
+
return str
|
|
30
|
+
.replaceAll(""", '"')
|
|
31
|
+
.replaceAll("'", "'")
|
|
32
|
+
.replaceAll("<", "<")
|
|
33
|
+
.replaceAll(">", ">")
|
|
34
|
+
.replaceAll("&", "&")
|
|
35
|
+
.replaceAll(/&#(\d+);/gu, (_, dec) =>
|
|
36
|
+
String.fromCodePoint(Math.trunc(Number(dec)))
|
|
37
|
+
)
|
|
38
|
+
.replaceAll(/&#x([0-9a-fA-F]+);/gu, (_, hex) =>
|
|
39
|
+
String.fromCodePoint(Number.parseInt(hex, 16))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Strips namespace prefix or Clark notation from a tag name.
|
|
45
|
+
* e.g. "{http://...}Rule" -> "Rule", "s3:Rule" -> "Rule", "Rule" -> "Rule"
|
|
46
|
+
*/
|
|
47
|
+
export function localTagName(tag: string): string {
|
|
48
|
+
if (tag.startsWith("{")) {
|
|
49
|
+
const idx = tag.indexOf("}");
|
|
50
|
+
if (idx !== -1) return tag.slice(idx + 1);
|
|
51
|
+
}
|
|
52
|
+
const colonIdx = tag.indexOf(":");
|
|
53
|
+
if (colonIdx !== -1) {
|
|
54
|
+
return tag.slice(colonIdx + 1);
|
|
55
|
+
}
|
|
56
|
+
return tag;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isString(
|
|
60
|
+
value: string | Record<string, string> | undefined
|
|
61
|
+
): value is string {
|
|
62
|
+
return typeof value === "string";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class XmlElement {
|
|
66
|
+
public tag: string;
|
|
67
|
+
public attrs: Record<string, string>;
|
|
68
|
+
public text?: string;
|
|
69
|
+
public children: XmlElement[];
|
|
70
|
+
|
|
71
|
+
constructor(tag: string, attrs: Record<string, string> = {}, text?: string) {
|
|
72
|
+
this.tag = tag;
|
|
73
|
+
this.attrs = { ...attrs };
|
|
74
|
+
this.text = text;
|
|
75
|
+
this.children = [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public localName(): string {
|
|
79
|
+
return localTagName(this.tag);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public subElement(
|
|
83
|
+
tag: string,
|
|
84
|
+
textOrAttrs?: string | Record<string, string>,
|
|
85
|
+
text?: string
|
|
86
|
+
): XmlElement {
|
|
87
|
+
const el = isString(textOrAttrs)
|
|
88
|
+
? new XmlElement(tag, {}, textOrAttrs)
|
|
89
|
+
: new XmlElement(tag, textOrAttrs || {}, text);
|
|
90
|
+
|
|
91
|
+
this.children.push(el);
|
|
92
|
+
return el;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public find(tag: string): XmlElement | undefined {
|
|
96
|
+
const targetLocal = localTagName(tag);
|
|
97
|
+
return this.children.find(
|
|
98
|
+
(c) => c.tag === tag || c.localName() === targetLocal
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public findAll(tag: string): XmlElement[] {
|
|
103
|
+
const targetLocal = localTagName(tag);
|
|
104
|
+
return this.children.filter(
|
|
105
|
+
(c) => c.tag === tag || c.localName() === targetLocal
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public findText(tag: string): string | undefined {
|
|
110
|
+
const el = this.find(tag);
|
|
111
|
+
return el?.text;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public findDescendant(tag: string): XmlElement | undefined {
|
|
115
|
+
const targetLocal = localTagName(tag);
|
|
116
|
+
for (const c of this.children) {
|
|
117
|
+
if (c.tag === tag || c.localName() === targetLocal) {
|
|
118
|
+
return c;
|
|
119
|
+
}
|
|
120
|
+
const deep = c.findDescendant(tag);
|
|
121
|
+
if (deep) return deep;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public findAllDescendants(tag: string): XmlElement[] {
|
|
127
|
+
const targetLocal = localTagName(tag);
|
|
128
|
+
const result: XmlElement[] = [];
|
|
129
|
+
for (const c of this.children) {
|
|
130
|
+
if (c.tag === tag || c.localName() === targetLocal) {
|
|
131
|
+
result.push(c);
|
|
132
|
+
}
|
|
133
|
+
result.push(...c.findAllDescendants(tag));
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public iter(): XmlElement[] {
|
|
139
|
+
const result: XmlElement[] = [this];
|
|
140
|
+
for (const child of this.children) {
|
|
141
|
+
result.push(...child.iter());
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public toXml(includeDeclaration = false): string {
|
|
147
|
+
let s = includeDeclaration ? `${XML_DECL}\n` : "";
|
|
148
|
+
s += `<${this.tag}`;
|
|
149
|
+
for (const [k, v] of Object.entries(this.attrs)) {
|
|
150
|
+
s += ` ${k}="${escapeXml(v)}"`;
|
|
151
|
+
}
|
|
152
|
+
if (this.children.length === 0 && this.text === undefined) {
|
|
153
|
+
s += "/>";
|
|
154
|
+
return s;
|
|
155
|
+
}
|
|
156
|
+
s += ">";
|
|
157
|
+
if (this.text !== undefined) {
|
|
158
|
+
s += escapeXml(this.text);
|
|
159
|
+
}
|
|
160
|
+
for (const child of this.children) {
|
|
161
|
+
s += child.toXml(false);
|
|
162
|
+
}
|
|
163
|
+
s += `</${this.tag}>`;
|
|
164
|
+
return s;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function subElement(
|
|
169
|
+
parent: XmlElement,
|
|
170
|
+
tag: string,
|
|
171
|
+
textOrAttrs?: string | Record<string, string>,
|
|
172
|
+
text?: string
|
|
173
|
+
): XmlElement {
|
|
174
|
+
return parent.subElement(tag, textOrAttrs, text);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// oxlint-disable-next-line complexity
|
|
178
|
+
export function parseXml(xml: string | Buffer): XmlElement {
|
|
179
|
+
// String inputs are also used by browser upload handles.
|
|
180
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof
|
|
181
|
+
let str = (typeof xml === "string" ? xml : xml.toString("utf-8")).trim();
|
|
182
|
+
if (!str) {
|
|
183
|
+
throw new ParseError("Empty XML document");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Strip XML declaration if present
|
|
187
|
+
if (str.startsWith("<?xml")) {
|
|
188
|
+
const end = str.indexOf("?>");
|
|
189
|
+
if (end !== -1) {
|
|
190
|
+
str = str.slice(end + 2).trim();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let pos = 0;
|
|
195
|
+
const len = str.length;
|
|
196
|
+
let root: XmlElement | null = null;
|
|
197
|
+
const stack: XmlElement[] = [];
|
|
198
|
+
|
|
199
|
+
while (pos < len) {
|
|
200
|
+
const lt = str.indexOf("<", pos);
|
|
201
|
+
if (lt === -1) {
|
|
202
|
+
if (stack.length > 0) {
|
|
203
|
+
const text = unescapeXml(str.slice(pos).trim());
|
|
204
|
+
const top = stack.at(-1);
|
|
205
|
+
if (text && top) {
|
|
206
|
+
top.text = (top.text || "") + text;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (lt > pos && stack.length > 0) {
|
|
213
|
+
const text = unescapeXml(str.slice(pos, lt).trim());
|
|
214
|
+
const top = stack.at(-1);
|
|
215
|
+
if (text && top) {
|
|
216
|
+
top.text = (top.text || "") + text;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pos = lt + 1;
|
|
221
|
+
|
|
222
|
+
// Comment
|
|
223
|
+
if (str.startsWith("!--", pos)) {
|
|
224
|
+
const end = str.indexOf("-->", pos + 3);
|
|
225
|
+
pos = end === -1 ? len : end + 3;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// CDATA
|
|
230
|
+
if (str.startsWith("![CDATA[", pos)) {
|
|
231
|
+
const end = str.indexOf("]]>", pos + 8);
|
|
232
|
+
const cdata = end === -1 ? str.slice(pos + 8) : str.slice(pos + 8, end);
|
|
233
|
+
const top = stack.at(-1);
|
|
234
|
+
if (top) {
|
|
235
|
+
top.text = (top.text || "") + cdata;
|
|
236
|
+
}
|
|
237
|
+
pos = end === -1 ? len : end + 3;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Processing instructions or DOCTYPE
|
|
242
|
+
if (str.startsWith("?", pos) || str.startsWith("!", pos)) {
|
|
243
|
+
const gt = str.indexOf(">", pos);
|
|
244
|
+
pos = gt === -1 ? len : gt + 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Closing tag: </tag>
|
|
249
|
+
if (str[pos] === "/") {
|
|
250
|
+
const gt = str.indexOf(">", pos);
|
|
251
|
+
if (gt === -1) throw new ParseError("Unclosed closing tag");
|
|
252
|
+
const closeTag = str.slice(pos + 1, gt).trim();
|
|
253
|
+
const open = stack.pop();
|
|
254
|
+
if (!open) {
|
|
255
|
+
throw new ParseError(`Unexpected closing tag: </${closeTag}>`);
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
localTagName(open.tag) !== localTagName(closeTag) &&
|
|
259
|
+
open.tag !== closeTag
|
|
260
|
+
) {
|
|
261
|
+
throw new ParseError(
|
|
262
|
+
`Mismatched closing tag: expected </${open.tag}>, got </${closeTag}>`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
pos = gt + 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Opening tag: <tag ...>
|
|
270
|
+
const gt = str.indexOf(">", pos);
|
|
271
|
+
if (gt === -1) {
|
|
272
|
+
throw new ParseError("Unclosed opening tag");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let tagBody = str.slice(pos, gt).trim();
|
|
276
|
+
const selfClosing = tagBody.endsWith("/");
|
|
277
|
+
if (selfClosing) {
|
|
278
|
+
tagBody = tagBody.slice(0, -1).trim();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const spaceIdx = tagBody.search(/\s/u);
|
|
282
|
+
const tagName = spaceIdx === -1 ? tagBody : tagBody.slice(0, spaceIdx);
|
|
283
|
+
const attrs: Record<string, string> = {};
|
|
284
|
+
|
|
285
|
+
if (spaceIdx !== -1) {
|
|
286
|
+
const attrStr = tagBody.slice(spaceIdx).trim();
|
|
287
|
+
const attrRegex =
|
|
288
|
+
/([a-zA-Z0-9_:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gu;
|
|
289
|
+
let m: RegExpExecArray | null;
|
|
290
|
+
while ((m = attrRegex.exec(attrStr)) !== null) {
|
|
291
|
+
attrs[m[1]] = unescapeXml(m[2] ?? m[3] ?? m[4] ?? "");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const el = new XmlElement(tagName, attrs);
|
|
296
|
+
const top = stack.at(-1);
|
|
297
|
+
if (top) {
|
|
298
|
+
top.children.push(el);
|
|
299
|
+
} else {
|
|
300
|
+
if (root) {
|
|
301
|
+
throw new ParseError("Multiple root elements found in XML document");
|
|
302
|
+
}
|
|
303
|
+
root = el;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!selfClosing) {
|
|
307
|
+
stack.push(el);
|
|
308
|
+
}
|
|
309
|
+
pos = gt + 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const lastUnclosed = stack.at(-1);
|
|
313
|
+
if (lastUnclosed) {
|
|
314
|
+
throw new ParseError(`Unclosed element: <${lastUnclosed.tag}>`);
|
|
315
|
+
}
|
|
316
|
+
if (!root) {
|
|
317
|
+
throw new ParseError("Empty XML document");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return root;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function xmlBody(root: XmlElement): Buffer {
|
|
324
|
+
return Buffer.from(`${XML_DECL}\n${root.toXml(false)}`, "utf-8");
|
|
325
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OUTPUT_DIR = "build";
|
|
8
|
+
const DEFAULT_GITIGNORE = ".gitignore";
|
|
9
|
+
const DEFAULT_EXCLUDES = [".git", "build"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the package name from package.json in the target directory.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} cwd - The project root directory.
|
|
15
|
+
* @returns {string} Sanitized package name or fallback "app".
|
|
16
|
+
*/
|
|
17
|
+
function getPackageName(cwd) {
|
|
18
|
+
const packageJsonPath = path.resolve(cwd, "package.json");
|
|
19
|
+
if (existsSync(packageJsonPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
22
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof
|
|
23
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) {
|
|
24
|
+
return pkg.name.trim().replace(/^@/u, "").replaceAll("/", "-");
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore parse errors and use fallback
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return "app";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Packages the current application directory into a timestamped tar.gz archive:
|
|
35
|
+
* `<package-name>-<timestamp>.tar.gz`
|
|
36
|
+
* Cleans and recreates the output directory before packaging.
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} [options] - Configuration options for packaging.
|
|
39
|
+
* @param {string} [options.cwd] - The project root directory. Defaults to `process.cwd()`.
|
|
40
|
+
* @param {string} [options.outputDirectory] - Output directory relative to `cwd`. Defaults to `"build"`.
|
|
41
|
+
* @param {string} [options.packageName] - Custom package name override. Defaults to name from `package.json`.
|
|
42
|
+
* @param {number} [options.timestamp] - Custom timestamp for the archive filename. Defaults to `Date.now()`.
|
|
43
|
+
* @param {string[]} [options.excludes] - Patterns to exclude from the archive.
|
|
44
|
+
* @param {string} [options.gitignorePath] - Gitignore file path relative to `cwd`. Defaults to `".gitignore"`.
|
|
45
|
+
* @returns {{ archiveName: string, relativePath: string, absolutePath: string, packageName: string }} Details of the generated archive.
|
|
46
|
+
*/
|
|
47
|
+
export function packageApp(options = {}) {
|
|
48
|
+
const cwd = options.cwd ?? process.cwd();
|
|
49
|
+
const outputDirectory = options.outputDirectory ?? DEFAULT_OUTPUT_DIR;
|
|
50
|
+
const targetDir = path.resolve(cwd, outputDirectory);
|
|
51
|
+
|
|
52
|
+
// Clean and recreate the output directory
|
|
53
|
+
if (existsSync(targetDir)) {
|
|
54
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(targetDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const packageName = options.packageName ?? getPackageName(cwd);
|
|
59
|
+
const timestamp = options.timestamp ?? Date.now();
|
|
60
|
+
const archiveName = `${timestamp}.tar.gz`;
|
|
61
|
+
const relativePath = path.posix.join(outputDirectory, archiveName);
|
|
62
|
+
const absolutePath = path.resolve(cwd, relativePath);
|
|
63
|
+
|
|
64
|
+
const gitignoreFile = options.gitignorePath ?? DEFAULT_GITIGNORE;
|
|
65
|
+
const excludes = options.excludes ?? DEFAULT_EXCLUDES;
|
|
66
|
+
|
|
67
|
+
const tarArgs = ["-czf", relativePath];
|
|
68
|
+
|
|
69
|
+
const fullGitignorePath = path.resolve(cwd, gitignoreFile);
|
|
70
|
+
if (existsSync(fullGitignorePath)) {
|
|
71
|
+
tarArgs.push(`--exclude-from=${gitignoreFile}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const exclude of excludes) {
|
|
75
|
+
tarArgs.push(`--exclude=${exclude}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
tarArgs.push(".");
|
|
79
|
+
|
|
80
|
+
const result = spawnSync("tar", tarArgs, {
|
|
81
|
+
cwd,
|
|
82
|
+
stdio: "inherit",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (result.error) {
|
|
86
|
+
throw result.error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
throw new Error(`tar process exited with status ${result.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
archiveName,
|
|
95
|
+
relativePath,
|
|
96
|
+
absolutePath,
|
|
97
|
+
packageName,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const [, entryPath] = process.argv;
|
|
102
|
+
if (
|
|
103
|
+
entryPath &&
|
|
104
|
+
import.meta.url === pathToFileURL(path.resolve(entryPath)).href
|
|
105
|
+
) {
|
|
106
|
+
try {
|
|
107
|
+
const result = packageApp();
|
|
108
|
+
console.log(`Successfully packaged application to ${result.relativePath}`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(
|
|
111
|
+
"Failed to package application:",
|
|
112
|
+
error instanceof Error ? error.message : error
|
|
113
|
+
);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
}
|