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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-monolayer-app",
3
- "version": "0.2.2",
4
- "description": "CLI to scaffold a new Monolayer application",
3
+ "version": "0.4.0",
4
+ "description": "CLI to scaffold a new monolayer application",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-monolayer-app": "./bin/index.js"
@@ -1,12 +1,17 @@
1
1
  .DS_Store
2
2
 
3
3
  .env
4
- /node_modules/
5
4
 
6
- # React Router
5
+ node_modules/
7
6
 
8
- /.react-router/
9
- /build/
7
+ # React Router
8
+ .react-router/
9
+ build/
10
10
 
11
11
  .pglite_development/
12
12
  .pglite_test/
13
+
14
+ .config/kysely.config.js
15
+ .config/kysely.config.js.map
16
+
17
+ .emulator/
@@ -0,0 +1,63 @@
1
+ import { FileBucket } from "../../vendor/file-bucket";
2
+ import { parseFileBucketRequest } from "../../vendor/file-bucket/protocol";
3
+ import {
4
+ dispatch,
5
+ MAX_BATCH_KEYS,
6
+ MAX_REQUEST_BYTES,
7
+ readLimitedJson,
8
+ type FileBucketRequest,
9
+ } from "../../vendor/file-bucket/react-router";
10
+
11
+ export const action = async ({
12
+ request,
13
+ }: {
14
+ request: Request;
15
+ }): Promise<Response> => {
16
+ if (request.method !== "POST") {
17
+ return new Response("Method not allowed", {
18
+ status: 405,
19
+ headers: { Allow: "POST" },
20
+ });
21
+ }
22
+
23
+ const contentTypeHeader = request.headers.get("content-type");
24
+ const mediaType = contentTypeHeader?.toLowerCase().split(";")[0]?.trim();
25
+ if (mediaType !== "application/json") {
26
+ return new Response("Unsupported Media Type", { status: 415 });
27
+ }
28
+
29
+ const contentLengthHeader = request.headers.get("content-length");
30
+ if (contentLengthHeader !== null) {
31
+ const declaredLength = Number(contentLengthHeader);
32
+ if (!Number.isNaN(declaredLength) && declaredLength > MAX_REQUEST_BYTES) {
33
+ return new Response("Request too large", { status: 413 });
34
+ }
35
+ }
36
+
37
+ let value: unknown;
38
+ try {
39
+ value = await readLimitedJson(request, MAX_REQUEST_BYTES);
40
+ } catch (error) {
41
+ if (error instanceof Response) return error;
42
+ return new Response("Invalid JSON", { status: 400 });
43
+ }
44
+
45
+ let input: Readonly<FileBucketRequest>;
46
+ try {
47
+ input = parseFileBucketRequest(value, { maxBatchKeys: MAX_BATCH_KEYS });
48
+ } catch {
49
+ return new Response("Invalid file request", { status: 400 });
50
+ }
51
+
52
+ const bucket = new FileBucket(input.bucketId);
53
+ const result = await dispatch(bucket, input);
54
+ if (result === undefined) {
55
+ return new Response(null, {
56
+ status: 204,
57
+ headers: { "Cache-Control": "no-store" },
58
+ });
59
+ }
60
+ return Response.json(result, {
61
+ headers: { "Cache-Control": "private, no-store" },
62
+ });
63
+ };
@@ -1,3 +1,6 @@
1
- import { type RouteConfig, index } from "@react-router/dev/routes";
1
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
2
2
 
3
- export default [index("routes/home.tsx")] satisfies RouteConfig;
3
+ export default [
4
+ index("routes/home.tsx"),
5
+ route("resources/files", "routes/files.ts"),
6
+ ] satisfies RouteConfig;
@@ -3,5 +3,10 @@ import ultracite from "ultracite/oxfmt";
3
3
 
4
4
  export default defineConfig({
5
5
  ...ultracite,
6
- ignorePatterns: [".agents/**"],
6
+ ignorePatterns: [
7
+ ".agents/**",
8
+ ".emulator/**",
9
+ "vendor/file-bucket/vite.js",
10
+ "vendor/file-bucket/vite.d.ts",
11
+ ],
7
12
  });
@@ -1,16 +1,12 @@
1
1
  import { defineConfig } from "oxlint";
2
2
  import antiSlop from "ultracite/oxlint/anti-slop";
3
3
  import core from "ultracite/oxlint/core";
4
- import { jsPluginSettings, selectJsPlugins } from "ultracite/oxlint/js-plugins";
5
4
  import react from "ultracite/oxlint/react";
6
5
  import remix from "ultracite/oxlint/remix";
7
6
 
8
- const jsPlugins = selectJsPlugins(["react-doctor"]);
9
-
10
7
  export default defineConfig({
11
- extends: [core, react, remix, antiSlop, jsPlugins],
12
- ignorePatterns: core.ignorePatterns,
13
- jsPlugins: jsPlugins.jsPlugins,
8
+ extends: [core, react, remix, antiSlop],
9
+ ignorePatterns: [...(core.ignorePatterns ?? []), "vendor/file-bucket/**"],
14
10
  rules: {
15
11
  "react/function-component-definition": [
16
12
  "error",
@@ -28,8 +24,8 @@ export default defineConfig({
28
24
  ],
29
25
  "no-use-before-define": "off",
30
26
  "no-empty-pattern": "off",
31
- "react-doctor/rendering-svg-precision": "off",
32
27
  "arrow-body-style": "off",
28
+ curly: "off",
33
29
  },
34
30
  overrides: [
35
31
  {
@@ -41,5 +37,4 @@ export default defineConfig({
41
37
  },
42
38
  },
43
39
  ],
44
- settings: jsPluginSettings,
45
40
  });
@@ -16,7 +16,8 @@
16
16
  "postmigrate:down": "npm run db:create-types",
17
17
  "migrate:list": "kysely migrate:list",
18
18
  "migrate:make": "tsx vendor/kysely-dsql/generator/create-migration.ts",
19
- "db:create-types": "tsx vendor/kysely-dsql/generator/generate-types-from-pglite.ts"
19
+ "db:create-types": "tsx vendor/kysely-dsql/generator/generate-types-from-pglite.ts",
20
+ "package-app": "node vendor/package-app/package-app.mjs"
20
21
  },
21
22
  "dependencies": {
22
23
  "@aws-sdk/dsql-signer": "^3.1126.0",
@@ -0,0 +1,196 @@
1
+ import type { FileBucket } from "./index.js";
2
+ import { createMultipartUploadClient } from "./multipart.js";
3
+ import { uploadSignedUrl } from "./transfers.js";
4
+ import { request } from "./transport.js";
5
+ import type { GetUrlOptions, PutOptions, PutUrlOptions } from "./types.js";
6
+
7
+ export { createMultipartUploadClient } from "./multipart.js";
8
+ export type { UploadGrant } from "./protocol.js";
9
+ export type {
10
+ FileMultipartUploadDescriptor,
11
+ FileMultipartUploadClient,
12
+ UploadOptions,
13
+ UploadProgress,
14
+ GetUrlOptions,
15
+ PutOptions,
16
+ PutUrlOptions,
17
+ } from "./types.js";
18
+ export type BrowserUploadBody =
19
+ | Blob
20
+ | string
21
+ | Uint8Array<ArrayBuffer>
22
+ | ArrayBuffer;
23
+
24
+ /**
25
+ * Delete a single object from storage via the signing resource endpoint.
26
+ *
27
+ * Deleting a missing key succeeds. Cancellation stops waiting but cannot undo
28
+ * server-side deletion. Endpoint failures reject the promise.
29
+ *
30
+ * @param bucket - The `FileBucket` target definition.
31
+ * @param path - Key of the object to delete.
32
+ * @param options - Optional settings including an `AbortSignal`.
33
+ * @returns A promise that resolves when the deletion is completed by the server.
34
+ */
35
+ function deleteFile(
36
+ bucket: FileBucket,
37
+ path: string,
38
+ options: { signal?: AbortSignal } = {}
39
+ ): Promise<void> {
40
+ return request(
41
+ { operation: "delete", bucketId: bucket.name, path },
42
+ options.signal
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Delete multiple objects from storage in batch via the signing resource endpoint.
48
+ *
49
+ * An empty list sends no request unless cancellation has already been requested,
50
+ * in which case the promise rejects. Batches are not atomic: some keys may be
51
+ * deleted before an error. Cancellation cannot undo completed deletions.
52
+ *
53
+ * @param bucket - The `FileBucket` target definition.
54
+ * @param paths - Array of object keys to delete.
55
+ * @param options - Optional settings including an `AbortSignal`.
56
+ * @returns A promise that resolves when batch deletion is completed by the server.
57
+ */
58
+ export async function deleteBatch(
59
+ bucket: FileBucket,
60
+ paths: string[],
61
+ options: { signal?: AbortSignal } = {}
62
+ ): Promise<void> {
63
+ options.signal?.throwIfAborted();
64
+ if (!paths.length) return;
65
+ await request(
66
+ { operation: "deleteBatch", bucketId: bucket.name, paths },
67
+ options.signal
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Request a presigned GET URL for an object from the server without initiating a download.
73
+ *
74
+ * Signing does not check object existence. Content disposition is unchanged by
75
+ * default; URL lifetime defaults to 3600 seconds. Cancellation applies to signing.
76
+ *
77
+ * @param bucket - The `FileBucket` target definition.
78
+ * @param path - Key of the object to access.
79
+ * @param options - Optional URL configuration such as `expiresIn`, `contentDisposition`, and `signal`.
80
+ * @returns A promise resolving to the presigned GET URL string.
81
+ */
82
+ export async function getUrl(
83
+ bucket: FileBucket,
84
+ path: string,
85
+ options: GetUrlOptions & { signal?: AbortSignal } = {}
86
+ ): Promise<string> {
87
+ const { signal, ...signingOptions } = options;
88
+ const result = await request(
89
+ { ...signingOptions, operation: "getUrl", bucketId: bucket.name, path },
90
+ signal
91
+ );
92
+ return result.url;
93
+ }
94
+
95
+ /**
96
+ * Request a presigned single-PUT upload URL from the server without sending file bytes.
97
+ *
98
+ * Send the raw body with the same content-type and cache-control headers used
99
+ * when signing. Metadata is carried in the signed query. No multipart session
100
+ * is created. URL lifetime defaults to 3600 seconds; cancellation applies to signing.
101
+ *
102
+ * @param bucket - The `FileBucket` target definition.
103
+ * @param path - Target key for the uploaded object.
104
+ * @param options - Optional URL configuration such as `expiresIn`, `contentType`, `metadata`, `cacheControl`, and `signal`.
105
+ * @returns A promise resolving to the presigned PUT URL string.
106
+ */
107
+ export async function putUrl(
108
+ bucket: FileBucket,
109
+ path: string,
110
+ options: PutUrlOptions & { signal?: AbortSignal } = {}
111
+ ): Promise<string> {
112
+ const { signal, ...signingOptions } = options;
113
+ const result = await request(
114
+ { ...signingOptions, operation: "putUrl", bucketId: bucket.name, path },
115
+ signal
116
+ );
117
+ return result.url;
118
+ }
119
+
120
+ /**
121
+ * Request an attachment download URL from the server and trigger native browser download navigation.
122
+ *
123
+ * Resolves as soon as the browser initiates the download link; does not buffer file bytes in memory.
124
+ * It cannot confirm saving or report subsequent storage errors. Cancellation
125
+ * applies before handoff and cannot stop the native download afterward.
126
+ *
127
+ * @param bucket - The `FileBucket` target definition.
128
+ * @param path - Key of the object to download.
129
+ * @param options - Optional settings including an `AbortSignal`.
130
+ * @returns A promise that resolves once the download has been triggered in the browser.
131
+ */
132
+ export async function get(
133
+ bucket: FileBucket,
134
+ path: string,
135
+ options: { signal?: AbortSignal } = {}
136
+ ): Promise<void> {
137
+ const grant = await request(
138
+ { operation: "get", bucketId: bucket.name, path },
139
+ options.signal
140
+ );
141
+ const link = document.createElement("a");
142
+ link.href = grant.url;
143
+ link.download = "";
144
+ link.hidden = true;
145
+ document.body.append(link);
146
+ try {
147
+ link.click();
148
+ } finally {
149
+ link.remove();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Upload a file or data directly to S3 from the browser using a server-issued grant.
155
+ *
156
+ * Uses `/resources/files` for signing, then sends bytes directly to storage.
157
+ * The standard resource action selects a single PUT below 8 MiB and multipart
158
+ * at or above 8 MiB. Reusing a key overwrites its object.
159
+ *
160
+ * Single PUT reports progress after completion; multipart reports completed part
161
+ * bytes and attempts cleanup on cancellation or failure. A Blob's content type
162
+ * is used unless overridden. Endpoint or storage failures reject the promise.
163
+ *
164
+ * @param bucket - The `FileBucket` target definition.
165
+ * @param path - Key of the destination object.
166
+ * @param body - The payload to upload (`Blob`, `File`, `string`, `Uint8Array`, or `ArrayBuffer`).
167
+ * @param options - Optional upload settings including `contentType`, `metadata`, `cacheControl`, `onProgress`, and `signal`.
168
+ * @returns A promise that resolves when the upload is confirmed complete.
169
+ */
170
+ export async function put(
171
+ bucket: FileBucket,
172
+ path: string,
173
+ body: BrowserUploadBody,
174
+ options: PutOptions = {}
175
+ ): Promise<void> {
176
+ options.signal?.throwIfAborted();
177
+ const file = body instanceof Blob ? body : new Blob([body]);
178
+ const grant = await request(
179
+ {
180
+ operation: "put",
181
+ bucketId: bucket.name,
182
+ path,
183
+ size: file.size,
184
+ contentType: options.contentType ?? (file.type || undefined),
185
+ metadata: options.metadata,
186
+ cacheControl: options.cacheControl,
187
+ },
188
+ options.signal
189
+ );
190
+ if (grant.type === "multipart") {
191
+ await createMultipartUploadClient(grant.descriptor).upload(file, options);
192
+ return;
193
+ }
194
+ await uploadSignedUrl(grant.url, file, grant.headers, options);
195
+ }
196
+ export { deleteFile as delete };
@@ -0,0 +1,26 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Modified
4
+
5
+ /**
6
+ * Error names returned by FileBucket actions.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * try {
11
+ * await put(bucket, 'report.txt', body);
12
+ * } catch (e: unknown) {
13
+ * if (e instanceof Error && e.name === FileBucketErrors.FileTooLarge) {
14
+ * // handle an oversized file
15
+ * }
16
+ * throw e;
17
+ * }
18
+ * ```
19
+ */
20
+ export const FileBucketErrors = {
21
+ FileNotFound: "NoSuchKey",
22
+ FileTooLarge: "EntityTooLarge",
23
+ UploadExpired: "UploadExpired",
24
+ UploadCompletionUnknown: "UploadCompletionUnknown",
25
+ InvalidUploadState: "InvalidUploadState",
26
+ } as const;
@@ -0,0 +1,7 @@
1
+ /** Shared file bucket definition. Safe to import on the server and in the browser. */
2
+ export class FileBucket {
3
+ readonly name: string;
4
+ constructor(name: string) {
5
+ this.name = name;
6
+ }
7
+ }