Skip to content

Commit 8c8818d

Browse files
committed
feat: support for astro v5
BREAKING CHANGE: Support for astro v5
1 parent 6cbdfd4 commit 8c8818d

File tree

9 files changed

+4709
-2179
lines changed

9 files changed

+4709
-2179
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import {defineConfig} from 'astro/config';
22
import react from '@astrojs/react';
33
import astroFormsDebug from "@astro-utils/forms/dist/integration.js";
4+
import node from '@astrojs/node';
45

56
// https://astro.build/config
67
export default defineConfig({
78
output: "server",
9+
adapter: node({
10+
mode: 'standalone',
11+
}),
812
integrations: [react(), astroFormsDebug]
913
});

examples/simple-form/package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@
1313
"dependencies": {
1414
"@astro-utils/express-endpoints": "^0.0.1",
1515
"@astro-utils/forms": "^0.0.1",
16-
"@astrojs/check": "^0.7.0",
17-
"@astrojs/react": "^3.6.0",
16+
"@astrojs/node": "^9.3.0",
17+
"@astrojs/react": "^4.3.0",
1818
"@types/react": "^18.3.3",
1919
"@types/react-dom": "^18.3.0",
20-
"astro": "^4.7.0",
20+
"astro": "^5.11.0",
2121
"bootstrap": "^5.3.2",
2222
"react": "^18.3.1",
2323
"react-dom": "^18.3.1",
2424
"reactstrap": "^9.2.1",
2525
"sleep-promise": "^9.1.0",
26-
"typescript": "^5.5.3",
2726
"zod": "^3.22.4"
27+
},
28+
"devDependencies": {
29+
"@astrojs/check": "^0.9.4",
30+
"typescript": "^5.8.3"
2831
}
29-
}
32+
}

package-lock.json

Lines changed: 4560 additions & 2100 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/forms/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,6 @@
8181
"zod": "^3.19.1"
8282
},
8383
"peerDependencies": {
84-
"astro": "^4.0.6"
84+
"astro": "^5.0.0"
8585
}
8686
}

packages/forms/src/components/WebForms.astro

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ if (webFormsSettings.haveFileUpload) {
2222
props.enctype = 'multipart/form-data';
2323
}
2424
25-
await Promise.all(
26-
disposeFiles.map(file => fs.unlink(file).catch(() => {}))
27-
);
25+
await Promise.all(disposeFiles.map(file => fs.unlink(file).catch(() => {
26+
})));
2827
2928
const useSession = getFormOptions(Astro).session?.cookieOptions?.maxAge;
3029
const formRequestToken = useSession && (await createFormToken(Astro));
@@ -45,7 +44,7 @@ const clientWFS = { loadingClassName, bigFileUploadOptions: bigFileClientOptions
4544
</script>
4645

4746
<script>
48-
import { BigFileUploadOptions, countTotalUploads } from './form/UploadBigFile/uploadBigFileClient.js';
47+
import { BigFileUploadOptions, countTotalUploads, finishFormSubmission, uploadAllFiles } from './form/UploadBigFile/uploadBigFileClient.js';
4948

5049
declare global {
5150
interface Window {
@@ -82,7 +81,7 @@ const clientWFS = { loadingClassName, bigFileUploadOptions: bigFileClientOptions
8281
const form = document.querySelector('form') as HTMLFormElement;
8382
form?.querySelectorAll('button[type="submit"]').forEach(button => {
8483
button.addEventListener('click', () => {
85-
if (button instanceof HTMLButtonElement === false || !button.formNoValidate && !form.checkValidity()) return;
84+
if (button instanceof HTMLButtonElement === false || (!button.formNoValidate && !form.checkValidity())) return;
8685

8786
if (window.clientWFS.loadingClassName) {
8887
button.classList.add(window.clientWFS.loadingClassName);
@@ -94,11 +93,31 @@ const clientWFS = { loadingClassName, bigFileUploadOptions: bigFileClientOptions
9493
});
9594
});
9695

97-
form?.addEventListener('submit', () => {
96+
form?.addEventListener('submit', event => {
9897
setTimeout(() => {
9998
form.querySelectorAll('button[type="submit"]').forEach(button => {
10099
(button as HTMLButtonElement).disabled = true;
101100
});
102101
}, 0);
102+
103+
// big files upload
104+
const allBigUploads = document.querySelectorAll('input[name].big-upload') as NodeListOf<HTMLInputElement>;
105+
const { count } = countTotalUploads(allBigUploads);
106+
if (count === 0) return;
107+
108+
let buttonCallback = '';
109+
if (document.activeElement instanceof HTMLButtonElement && document.activeElement.name === 'button-callback') {
110+
buttonCallback = document.activeElement.value;
111+
}
112+
113+
event.preventDefault();
114+
event.stopPropagation();
115+
116+
handleUploads(allBigUploads, buttonCallback);
103117
});
118+
119+
async function handleUploads(allBigUploads: NodeListOf<HTMLInputElement>, clickAfter?: string) {
120+
await uploadAllFiles(allBigUploads);
121+
finishFormSubmission(form, clickAfter);
122+
}
104123
</script>

packages/forms/src/components/form/UploadBigFile/UploadBigFile.astro

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,4 @@ export interface Props<T extends keyof JSX.IntrinsicElements | React.JSXElementC
99
const { class: className, tempDirectory, name, ...props } = Astro.props;
1010
await processBigFileUpload(Astro);
1111
---
12-
13-
<BInput type='file' {...props} name={name} class:list={[className, 'big-upload']} />
14-
15-
<script>
16-
import { countTotalUploads, finishFormSubmission, uploadAllFiles } from './uploadBigFileClient.js';
17-
18-
const form = document.querySelector('form') as HTMLFormElement;
19-
form?.addEventListener('submit', event => {
20-
const allBigUploads = document.querySelectorAll('input[name].big-upload') as NodeListOf<HTMLInputElement>;
21-
const { count } = countTotalUploads(allBigUploads);
22-
if (count === 0) return;
23-
24-
let buttonCallback = "";
25-
if(document.activeElement instanceof HTMLButtonElement && document.activeElement.name === "button-callback") {
26-
buttonCallback = document.activeElement.value;
27-
}
28-
29-
event.preventDefault();
30-
event.stopPropagation();
31-
32-
handleUploads(allBigUploads, buttonCallback);
33-
});
34-
35-
async function handleUploads(allBigUploads: NodeListOf<HTMLInputElement>, clickAfter?: string) {
36-
await uploadAllFiles(allBigUploads);
37-
finishFormSubmission(form, clickAfter);
38-
}
39-
</script>
12+
<BInput type='file' {...props} name={name} class:list={[className, 'big-upload']} />

packages/forms/src/components/form/UploadBigFile/uploadBigFileClient.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { v4 as uuid } from 'uuid';
1+
import {v4 as uuid} from 'uuid';
22

33
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
44

@@ -95,7 +95,7 @@ async function finishUpload(uploadId: string, options: BigFileUploadOptions) {
9595

9696
await sleep(options.waitFinishDelay);
9797
} catch (error) {
98-
if(maxError === 0){
98+
if (maxError === 0) {
9999
throw error;
100100
}
101101
maxError--;
@@ -133,6 +133,7 @@ async function uploadBigFile(fileId: string, file: File, progressCallback: Progr
133133
total: totalChunks,
134134
};
135135

136+
const stopRetrying = new AbortController();
136137
const uploadPromiseWithRetry = retry(async () => {
137138
const upload = await uploadChunkWithXHR(chunk, info, (loaded) => {
138139
activeLoads.set(i, loaded);
@@ -151,9 +152,12 @@ async function uploadBigFile(fileId: string, file: File, progressCallback: Progr
151152
}
152153

153154
if (!response?.ok) {
155+
if (response.retry === false) {
156+
stopRetrying.abort('Not retryable error');
157+
}
154158
throw new Error(response.error);
155159
}
156-
}, { retries: options.retryChunks, delay: options.retryDelay })
160+
}, {retries: options.retryChunks, delay: options.retryDelay, stopRetying: stopRetrying.signal})
157161
.then(() => {
158162
activeLoads.delete(i);
159163
activeChunks.delete(uploadPromiseWithRetry);
@@ -271,18 +275,24 @@ export function finishFormSubmission(form: HTMLFormElement, onClick?: string) {
271275
form.submit();
272276
}
273277

274-
async function retry(fn: () => Promise<void>, options: { retries: number, delay: number; } = { retries: 5, delay: 1000 }) {
278+
type RetryOptions = {
279+
retries: number,
280+
delay: number;
281+
stopRetying?: AbortSignal;
282+
};
283+
284+
async function retry(fn: () => Promise<void>, options: RetryOptions = {retries: 5, delay: 1000}) {
275285
let attempts = 0;
276286
while (attempts < options.retries) {
277287
try {
278288
await fn();
279289
return;
280290
} catch (error) {
281291
attempts++;
282-
if (attempts >= options.retries) {
292+
if (attempts >= options.retries || options.stopRetying?.aborted) {
283293
throw error;
284294
}
285295
await sleep(options.delay);
286296
}
287297
}
288-
}
298+
}

packages/forms/src/components/form/UploadBigFile/uploadBigFileServer.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import fsExtra from "fs-extra/esm";
2-
import fs from "fs/promises";
3-
import oldFs from "fs";
4-
import path from "path";
5-
import z from "zod";
6-
import os from "os";
7-
import { validateFrom } from "../../../form-tools/csrf.js";
8-
import { AstroGlobal } from "astro";
9-
import { getFormValue } from "../../../form-tools/post.js";
1+
import fsExtra from 'fs-extra/esm';
2+
import fs from 'fs/promises';
3+
import oldFs from 'fs';
4+
import path from 'path';
5+
import z from 'zod';
6+
import os from 'os';
7+
import {validateFrom} from '../../../form-tools/csrf.js';
8+
import {AstroGlobal} from 'astro';
9+
import {getFormValue} from '../../../form-tools/post.js';
1010
import ThrowOverrideResponse from '../../../throw-action/throwOverrideResponse.js';
1111

1212
const zodValidationInfo =
@@ -91,12 +91,12 @@ async function loadUploadFiles(astro: AstroGlobal, options: Partial<LoadUploadFi
9191

9292
if (typeof allowUpload === "function") {
9393
if (!await allowUpload(uploadFile, data)) {
94-
return await sendError("File not allowed");
94+
return await sendError('File not allowed', true, {retry: false});
9595
}
9696
}
9797

9898
if (uploadSize > maxUploadSize) {
99-
return await sendError("File size exceeded");
99+
return await sendError('File size exceeded', true, {retry: false});
100100
}
101101

102102
const totalDirectorySizeWithNewUpload = (await totalDirectorySize(tempDirectory)) + part === 1 ? uploadSize : uploadFile.size;
@@ -221,4 +221,4 @@ export async function checkIfFileExists(filePath: string) {
221221
} catch {
222222
return false;
223223
}
224-
}
224+
}
Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,90 @@
1+
const ASYNC_RENDERS_REGEX = /render\s*\(\s*destination\s*\)\s*{\s*const\s+flushers\s*=\s*this\.expressions\.map\s*\(\s*\(?exp\)?\s*=>\s*{\s*return\s+createBufferedRenderer\s*\(\s*destination\s*,\s*\(?bufferDestination\)?\s*=>\s*{\s*if\s*\(\s*exp\s*\|\|\s*exp\s*===\s*0\s*\)\s*{\s*return\s+renderChild\s*\(\s*bufferDestination\s*,\s*exp\s*\);\s*}\s*}\s*\);\s*}\s*\);/gs;
12

2-
/**
3-
const expRenders = this.expressions.map(exp => {
4-
return renderToBufferDestination(bufferDestination => {
5-
if (exp || exp === 0) {
6-
return renderChild(bufferDestination, exp);
3+
const SYNC_RENDERS_CODE_HELPERS = `
4+
const locks = new Map();
5+
async function withLock(scope, key, acquireLockSignalOrCallback, maybeCallback) {
6+
const signal = acquireLockSignalOrCallback instanceof AbortSignal
7+
? acquireLockSignalOrCallback
8+
: undefined;
9+
10+
const callback = typeof acquireLockSignalOrCallback === 'function'
11+
? acquireLockSignalOrCallback
12+
: maybeCallback;
13+
14+
if (typeof callback !== 'function') {
15+
throw new Error("callback is required");
716
}
8-
});
9-
});
10-
*/
11-
const ASYNC_RENDERS_REGEX = /const\s+expRenders\s*=\s*this\.expressions\.map\s*\(\s*\(?exp\)?\s*=>\s*{\s*return\s+renderToBufferDestination\s*\(\s*\(?bufferDestination\)?\s*=>\s*{\s*if\s*\(\s*exp\s*\|\|\s*exp\s*===\s*0\s*\)\s*{\s*return\s+renderChild\s*\(\s*bufferDestination\s*,\s*exp\s*\);\s*}\s*}\s*\);\s*}\s*\);/gs;
1217
13-
const SYNC_RENDERS_CODE = `
14-
const expRenders = [];
15-
for (const exp of this.expressions) {
16-
const promise = renderToBufferDestination(bufferDestination => {
17-
if (exp || exp === 0) {
18-
return renderChild(bufferDestination, exp);
18+
if (signal?.aborted) {
19+
throw signal.reason;
1920
}
20-
});
2121
22-
await promise.renderPromise;
23-
expRenders.push(promise);
24-
}
22+
let keyMap = locks.get(scope);
23+
if (!keyMap) {
24+
keyMap = new Map();
25+
locks.set(scope, keyMap);
26+
}
27+
28+
let entry = keyMap.get(key);
29+
let queue, onDelete;
30+
31+
if (entry) {
32+
[queue, onDelete] = entry;
33+
34+
await new Promise((resolve, reject) => {
35+
const onResolve = () => {
36+
signal?.removeEventListener("abort", onAbort);
37+
resolve();
38+
};
39+
40+
const onAbort = () => {
41+
const index = queue.indexOf(onResolve);
42+
if (index >= 0) queue.splice(index, 1);
43+
signal.removeEventListener("abort", onAbort);
44+
reject(signal.reason);
45+
};
46+
47+
queue.push(onResolve);
48+
if (signal) signal.addEventListener("abort", onAbort);
49+
});
50+
51+
} else {
52+
queue = [];
53+
onDelete = [];
54+
keyMap.set(key, [queue, onDelete]);
55+
}
56+
57+
try {
58+
return await callback.call(scope);
59+
} finally {
60+
if (queue.length > 0) {
61+
queue.shift()?.();
62+
} else {
63+
keyMap.delete(key);
64+
if (keyMap.size === 0) {
65+
locks.delete(scope);
66+
}
67+
for (const fn of onDelete) fn();
68+
}
69+
}
70+
}`;
71+
72+
const SYNC_RENDERS_CODE = `
73+
render(destination) {
74+
const flushers = this.expressions.map((exp) => {
75+
return createBufferedRenderer(destination, (bufferDestination) => {
76+
// Skip render if falsy, except the number 0
77+
if (exp || exp === 0) {
78+
return withLock(this, '_lockRender', () => renderChild(bufferDestination, exp));
79+
}
80+
});
81+
});
2582
`;
2683

2784
export function refactorCodeInlineRenderComponent(sourceCode: string): string {
28-
return sourceCode.replace(ASYNC_RENDERS_REGEX, SYNC_RENDERS_CODE);
29-
}
85+
if (ASYNC_RENDERS_REGEX.test(sourceCode)) {
86+
return SYNC_RENDERS_CODE_HELPERS + sourceCode.replace(ASYNC_RENDERS_REGEX, SYNC_RENDERS_CODE);
87+
}
88+
89+
return sourceCode;
90+
}

0 commit comments

Comments
 (0)