elm-pages 3.0.27 → 3.0.28
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/README.md +2 -2
- package/codegen/elm-pages-codegen.cjs +927 -570
- package/generator/review/elm.json +1 -1
- package/generator/src/compatibility-key.js +2 -2
- package/generator/src/render.js +194 -53
- package/generator/template/elm.json +1 -1
- package/generator/template/script/elm.json +1 -1
- package/package.json +1 -1
- package/src/BackendTask/Stream.elm +52 -37
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +1 -1
- package/src/Pages/Script.elm +73 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const compatibilityKey =
|
|
1
|
+
export const compatibilityKey = 23;
|
|
2
2
|
|
|
3
|
-
export const packageVersion = "3.0.
|
|
3
|
+
export const packageVersion = "3.0.28";
|
package/generator/src/render.js
CHANGED
|
@@ -536,6 +536,8 @@ async function runInternalJob(
|
|
|
536
536
|
return [requestHash, await runWhich(requestToPerform)];
|
|
537
537
|
case "elm-pages-internal://question":
|
|
538
538
|
return [requestHash, await runQuestion(requestToPerform)];
|
|
539
|
+
case "elm-pages-internal://readKey":
|
|
540
|
+
return [requestHash, await runReadKey(requestToPerform)];
|
|
539
541
|
case "elm-pages-internal://shell":
|
|
540
542
|
return [requestHash, await runShell(requestToPerform)];
|
|
541
543
|
case "elm-pages-internal://stream":
|
|
@@ -638,6 +640,10 @@ async function runQuestion(req) {
|
|
|
638
640
|
return jsonResponse(req, await question(req.body.args[0]));
|
|
639
641
|
}
|
|
640
642
|
|
|
643
|
+
async function runReadKey(req) {
|
|
644
|
+
return jsonResponse(req, await readKey());
|
|
645
|
+
}
|
|
646
|
+
|
|
641
647
|
function runStream(req, portsFile) {
|
|
642
648
|
return new Promise(async (resolve) => {
|
|
643
649
|
const context = getContext(req);
|
|
@@ -667,19 +673,21 @@ function runStream(req, portsFile) {
|
|
|
667
673
|
index += 1;
|
|
668
674
|
}
|
|
669
675
|
if (kind === "json") {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
+
try {
|
|
677
|
+
const body = await consumers.json(lastStream);
|
|
678
|
+
const metadata = await tryCallingFunction(metadataResponse);
|
|
679
|
+
resolve(jsonResponse(req, { body, metadata }));
|
|
680
|
+
} catch (error) {
|
|
681
|
+
resolve(jsonResponse(req, { error: error.toString() }));
|
|
682
|
+
}
|
|
676
683
|
} else if (kind === "text") {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
684
|
+
try {
|
|
685
|
+
const body = await consumers.text(lastStream);
|
|
686
|
+
const metadata = await tryCallingFunction(metadataResponse);
|
|
687
|
+
resolve(jsonResponse(req, { body, metadata }));
|
|
688
|
+
} catch (error) {
|
|
689
|
+
resolve(jsonResponse(req, { error: error.toString() }));
|
|
690
|
+
}
|
|
683
691
|
} else if (kind === "none") {
|
|
684
692
|
if (!lastStream) {
|
|
685
693
|
// ensure all error handling gets a chance to fire before resolving successfully
|
|
@@ -687,22 +695,21 @@ function runStream(req, portsFile) {
|
|
|
687
695
|
resolve(jsonResponse(req, { body: null }));
|
|
688
696
|
} else {
|
|
689
697
|
let resolvedMeta = await tryCallingFunction(metadataResponse);
|
|
690
|
-
|
|
698
|
+
// Writable streams emit "finish", Readable streams emit "end"
|
|
699
|
+
// Duplex streams emit both - use a flag to prevent double-resolve
|
|
700
|
+
let resolved = false;
|
|
701
|
+
const onComplete = () => {
|
|
702
|
+
if (resolved) return;
|
|
703
|
+
resolved = true;
|
|
691
704
|
resolve(
|
|
692
705
|
jsonResponse(req, {
|
|
693
706
|
body: null,
|
|
694
707
|
metadata: resolvedMeta,
|
|
695
708
|
})
|
|
696
709
|
);
|
|
697
|
-
}
|
|
698
|
-
lastStream.once("
|
|
699
|
-
|
|
700
|
-
jsonResponse(req, {
|
|
701
|
-
body: null,
|
|
702
|
-
metadata: resolvedMeta,
|
|
703
|
-
})
|
|
704
|
-
);
|
|
705
|
-
});
|
|
710
|
+
};
|
|
711
|
+
lastStream.once("finish", onComplete);
|
|
712
|
+
lastStream.once("end", onComplete);
|
|
706
713
|
}
|
|
707
714
|
} else if (kind === "command") {
|
|
708
715
|
// already handled in parts.forEach
|
|
@@ -745,19 +752,36 @@ function runStream(req, portsFile) {
|
|
|
745
752
|
env,
|
|
746
753
|
});
|
|
747
754
|
if (validateStream.isDuplexStream(newLocal.stream)) {
|
|
755
|
+
newLocal.stream.once("error", (error) => {
|
|
756
|
+
newLocal.stream.destroy();
|
|
757
|
+
resolve({ error: `Custom duplex stream '${part.portName}' error: ${error.message}` });
|
|
758
|
+
});
|
|
748
759
|
pipeIfPossible(lastStream, newLocal.stream);
|
|
760
|
+
if (!lastStream) {
|
|
761
|
+
endStreamIfNoInput(newLocal.stream);
|
|
762
|
+
}
|
|
749
763
|
return newLocal;
|
|
750
764
|
} else {
|
|
751
765
|
throw `Expected '${part.portName}' to be a duplex stream!`;
|
|
752
766
|
}
|
|
753
767
|
} else if (part.name === "customRead") {
|
|
768
|
+
const newLocal = await portsFile[part.portName](part.input, {
|
|
769
|
+
cwd,
|
|
770
|
+
quiet,
|
|
771
|
+
env,
|
|
772
|
+
});
|
|
773
|
+
// customRead can return either a stream directly or { stream, metadata }
|
|
774
|
+
const stream = newLocal.stream || newLocal;
|
|
775
|
+
if (!validateStream.isReadableStream(stream)) {
|
|
776
|
+
throw `Expected '${part.portName}' to return a readable stream!`;
|
|
777
|
+
}
|
|
778
|
+
stream.once("error", (error) => {
|
|
779
|
+
stream.destroy();
|
|
780
|
+
resolve({ error: `Custom read stream '${part.portName}' error: ${error.message}` });
|
|
781
|
+
});
|
|
754
782
|
return {
|
|
755
|
-
metadata: null,
|
|
756
|
-
stream:
|
|
757
|
-
cwd,
|
|
758
|
-
quiet,
|
|
759
|
-
env,
|
|
760
|
-
}),
|
|
783
|
+
metadata: newLocal.metadata || null,
|
|
784
|
+
stream: stream,
|
|
761
785
|
};
|
|
762
786
|
} else if (part.name === "customWrite") {
|
|
763
787
|
const newLocal = await portsFile[part.portName](part.input, {
|
|
@@ -766,25 +790,42 @@ function runStream(req, portsFile) {
|
|
|
766
790
|
env,
|
|
767
791
|
});
|
|
768
792
|
if (!validateStream.isWritableStream(newLocal.stream)) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
793
|
+
throw `Expected '${part.portName}' to return a writable stream!`;
|
|
794
|
+
}
|
|
795
|
+
newLocal.stream.once("error", (error) => {
|
|
796
|
+
newLocal.stream.destroy();
|
|
797
|
+
resolve({ error: `Custom write stream '${part.portName}' error: ${error.message}` });
|
|
798
|
+
});
|
|
799
|
+
pipeIfPossible(lastStream, newLocal.stream);
|
|
800
|
+
if (!lastStream) {
|
|
801
|
+
endStreamIfNoInput(newLocal.stream);
|
|
773
802
|
}
|
|
774
803
|
return newLocal;
|
|
775
804
|
} else if (part.name === "gzip") {
|
|
776
805
|
const gzip = zlib.createGzip();
|
|
806
|
+
gzip.once("error", (error) => {
|
|
807
|
+
gzip.destroy();
|
|
808
|
+
resolve({ error: `gzip error: ${error.message}` });
|
|
809
|
+
});
|
|
777
810
|
if (!lastStream) {
|
|
778
|
-
gzip
|
|
811
|
+
endStreamIfNoInput(gzip);
|
|
779
812
|
}
|
|
780
813
|
return {
|
|
781
814
|
metadata: null,
|
|
782
815
|
stream: pipeIfPossible(lastStream, gzip),
|
|
783
816
|
};
|
|
784
817
|
} else if (part.name === "unzip") {
|
|
818
|
+
const unzip = zlib.createUnzip();
|
|
819
|
+
unzip.once("error", (error) => {
|
|
820
|
+
unzip.destroy();
|
|
821
|
+
resolve({ error: `unzip error: ${error.message}` });
|
|
822
|
+
});
|
|
823
|
+
if (!lastStream) {
|
|
824
|
+
endStreamIfNoInput(unzip);
|
|
825
|
+
}
|
|
785
826
|
return {
|
|
786
827
|
metadata: null,
|
|
787
|
-
stream: pipeIfPossible(lastStream,
|
|
828
|
+
stream: pipeIfPossible(lastStream, unzip),
|
|
788
829
|
};
|
|
789
830
|
} else if (part.name === "fileWrite") {
|
|
790
831
|
const destinationPath = path.resolve(part.path);
|
|
@@ -801,9 +842,13 @@ function runStream(req, portsFile) {
|
|
|
801
842
|
newLocal.removeAllListeners();
|
|
802
843
|
resolve({ error: error.toString() });
|
|
803
844
|
});
|
|
845
|
+
pipeIfPossible(lastStream, newLocal);
|
|
846
|
+
if (!lastStream) {
|
|
847
|
+
endStreamIfNoInput(newLocal);
|
|
848
|
+
}
|
|
804
849
|
return {
|
|
805
850
|
metadata: null,
|
|
806
|
-
stream:
|
|
851
|
+
stream: newLocal,
|
|
807
852
|
};
|
|
808
853
|
} else if (part.name === "httpWrite") {
|
|
809
854
|
const makeFetchHappen = makeFetchHappenOriginal.defaults({
|
|
@@ -865,6 +910,9 @@ function runStream(req, portsFile) {
|
|
|
865
910
|
});
|
|
866
911
|
|
|
867
912
|
pipeIfPossible(lastStream, newProcess.stdin);
|
|
913
|
+
if (!lastStream) {
|
|
914
|
+
endStreamIfNoInput(newProcess.stdin);
|
|
915
|
+
}
|
|
868
916
|
let newStream;
|
|
869
917
|
if (output === "MergeWithStdout") {
|
|
870
918
|
newStream = mergeStreams([newProcess.stdout, newProcess.stderr]);
|
|
@@ -874,28 +922,40 @@ function runStream(req, portsFile) {
|
|
|
874
922
|
newStream = newProcess.stdout;
|
|
875
923
|
}
|
|
876
924
|
|
|
925
|
+
// For the last process, we need to track metadata resolution
|
|
926
|
+
// so we can resolve it even if the process errors
|
|
927
|
+
let resolveMeta = null;
|
|
928
|
+
const metadataPromise = isLastProcess
|
|
929
|
+
? new Promise((resolve) => {
|
|
930
|
+
resolveMeta = resolve;
|
|
931
|
+
})
|
|
932
|
+
: null;
|
|
933
|
+
|
|
877
934
|
newProcess.once("error", (error) => {
|
|
878
935
|
newStream && newStream.end();
|
|
879
936
|
newProcess.removeAllListeners();
|
|
937
|
+
// Resolve metadata Promise to prevent hanging awaits
|
|
938
|
+
if (resolveMeta) {
|
|
939
|
+
resolveMeta({ exitCode: null, error: error.toString() });
|
|
940
|
+
}
|
|
880
941
|
resolve({ error: error.toString() });
|
|
881
942
|
});
|
|
943
|
+
|
|
882
944
|
if (isLastProcess) {
|
|
945
|
+
newProcess.once("exit", (code) => {
|
|
946
|
+
if (code !== 0 && !allowNon0Status) {
|
|
947
|
+
newStream && newStream.end();
|
|
948
|
+
resolve({
|
|
949
|
+
error: `Command ${command} exited with code ${code}`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
resolveMeta({
|
|
953
|
+
exitCode: code,
|
|
954
|
+
});
|
|
955
|
+
});
|
|
883
956
|
return {
|
|
884
957
|
stream: newStream,
|
|
885
|
-
metadata:
|
|
886
|
-
newProcess.once("exit", (code) => {
|
|
887
|
-
if (code !== 0 && !allowNon0Status) {
|
|
888
|
-
newStream && newStream.end();
|
|
889
|
-
resolve({
|
|
890
|
-
error: `Command ${command} exited with code ${code}`,
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
resoveMeta({
|
|
895
|
-
exitCode: code,
|
|
896
|
-
});
|
|
897
|
-
});
|
|
898
|
-
}),
|
|
958
|
+
metadata: metadataPromise,
|
|
899
959
|
};
|
|
900
960
|
} else {
|
|
901
961
|
return { metadata: null, stream: newStream };
|
|
@@ -930,6 +990,45 @@ function pipeIfPossible(input, destination) {
|
|
|
930
990
|
}
|
|
931
991
|
}
|
|
932
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Safely signals EOF to a writable stream when no input will be piped to it.
|
|
995
|
+
*
|
|
996
|
+
* This is necessary because when a writable stream (like a child process's stdin)
|
|
997
|
+
* is created but nothing is piped to it, the receiving end has no way to know
|
|
998
|
+
* that no data is coming. It will wait indefinitely for the pipe to close.
|
|
999
|
+
*
|
|
1000
|
+
* GUI applications like ksdiff/meld are particularly affected - they wait for
|
|
1001
|
+
* stdin to close before proceeding, causing hangs if we don't explicitly end it.
|
|
1002
|
+
*
|
|
1003
|
+
* @param {import('stream').Writable | null | undefined} stream - The writable stream to end
|
|
1004
|
+
*/
|
|
1005
|
+
function endStreamIfNoInput(stream) {
|
|
1006
|
+
if (!stream) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Check if stream is still in a state where .end() is valid
|
|
1011
|
+
// - writable: false if the stream has been destroyed or ended
|
|
1012
|
+
// - writableEnded: true if .end() has already been called
|
|
1013
|
+
// - destroyed: true if .destroy() has been called
|
|
1014
|
+
if (!stream.writable || stream.writableEnded || stream.destroyed) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Add a one-time error handler to prevent unhandled error crashes
|
|
1019
|
+
// This can happen if the child process exits before we call .end()
|
|
1020
|
+
stream.once("error", (err) => {
|
|
1021
|
+
// EPIPE: "broken pipe" - the other end closed before we finished
|
|
1022
|
+
// This is expected if the child process exits quickly
|
|
1023
|
+
if (err.code !== "EPIPE") {
|
|
1024
|
+
// Log unexpected errors but don't crash - this is cleanup code
|
|
1025
|
+
console.error("Stream end error:", err.message);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
stream.end();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
933
1032
|
function stdout() {
|
|
934
1033
|
return new Writable({
|
|
935
1034
|
write(chunk, encoding, callback) {
|
|
@@ -1077,7 +1176,7 @@ export function pipeShells(
|
|
|
1077
1176
|
if (previousProcess === null) {
|
|
1078
1177
|
currentProcess = spawnCallback(command, args, {
|
|
1079
1178
|
stdio: ["inherit", "pipe", "inherit"],
|
|
1080
|
-
timeout: timeout
|
|
1179
|
+
timeout: timeout,
|
|
1081
1180
|
cwd: cwd,
|
|
1082
1181
|
env: env,
|
|
1083
1182
|
});
|
|
@@ -1087,14 +1186,14 @@ export function pipeShells(
|
|
|
1087
1186
|
stdio: quiet
|
|
1088
1187
|
? ["pipe", "ignore", "ignore"]
|
|
1089
1188
|
: ["pipe", "inherit", "inherit"],
|
|
1090
|
-
timeout: timeout
|
|
1189
|
+
timeout: timeout,
|
|
1091
1190
|
cwd: cwd,
|
|
1092
1191
|
env: env,
|
|
1093
1192
|
});
|
|
1094
1193
|
} else {
|
|
1095
1194
|
currentProcess = spawnCallback(command, args, {
|
|
1096
1195
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1097
|
-
timeout: timeout
|
|
1196
|
+
timeout: timeout,
|
|
1098
1197
|
cwd: cwd,
|
|
1099
1198
|
env: env,
|
|
1100
1199
|
});
|
|
@@ -1153,6 +1252,48 @@ export async function question({ prompt }) {
|
|
|
1153
1252
|
});
|
|
1154
1253
|
}
|
|
1155
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* Read a single keypress from stdin without requiring Enter.
|
|
1257
|
+
* Uses raw mode to capture individual keypresses.
|
|
1258
|
+
* Falls back to line-buffered input when not in a TTY (e.g., piped input).
|
|
1259
|
+
*/
|
|
1260
|
+
export async function readKey() {
|
|
1261
|
+
const stdin = process.stdin;
|
|
1262
|
+
|
|
1263
|
+
if (!stdin.isTTY) {
|
|
1264
|
+
// Fall back to reading a line when not in a TTY (piped input, CI, etc.)
|
|
1265
|
+
// Takes the first character of the input line
|
|
1266
|
+
const rl = readline.createInterface({ input: stdin });
|
|
1267
|
+
return new Promise((resolve) => {
|
|
1268
|
+
rl.once("line", (line) => {
|
|
1269
|
+
rl.close();
|
|
1270
|
+
resolve(line.charAt(0) || "\n");
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// TTY mode - single keypress without Enter
|
|
1276
|
+
return new Promise((resolve) => {
|
|
1277
|
+
const wasRaw = stdin.isRaw;
|
|
1278
|
+
|
|
1279
|
+
stdin.setRawMode(true);
|
|
1280
|
+
stdin.resume();
|
|
1281
|
+
stdin.setEncoding("utf8");
|
|
1282
|
+
|
|
1283
|
+
stdin.once("data", (key) => {
|
|
1284
|
+
stdin.setRawMode(wasRaw);
|
|
1285
|
+
stdin.pause();
|
|
1286
|
+
|
|
1287
|
+
// Handle Ctrl+C to exit gracefully
|
|
1288
|
+
if (key === "\u0003") {
|
|
1289
|
+
process.exit();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
resolve(key);
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1156
1297
|
async function runWriteFileJob(req) {
|
|
1157
1298
|
const { cwd } = getContext(req);
|
|
1158
1299
|
const data = req.body.args[0];
|
|
@@ -1223,7 +1364,7 @@ async function runGlobNew(req, patternsToWatch) {
|
|
|
1223
1364
|
}
|
|
1224
1365
|
return {
|
|
1225
1366
|
fullPath: fullPath.path,
|
|
1226
|
-
captures: mm.capture(pattern, fullPath.path),
|
|
1367
|
+
captures: mm.capture(pattern, fullPath.path) || [],
|
|
1227
1368
|
fileStats: {
|
|
1228
1369
|
size: stats.size,
|
|
1229
1370
|
atime: Math.round(stats.atime.getTime()),
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"elm-community/result-extra": "2.4.0",
|
|
32
32
|
"jluckyiv/elm-utc-date-strings": "1.0.0",
|
|
33
33
|
"justinmimbs/date": "4.1.0",
|
|
34
|
-
"mdgriffith/elm-codegen": "
|
|
34
|
+
"mdgriffith/elm-codegen": "6.0.1",
|
|
35
35
|
"miniBill/elm-codec": "2.2.0",
|
|
36
36
|
"noahzgordon/elm-color-extra": "1.0.2",
|
|
37
37
|
"robinheghan/fnv1a": "1.0.0",
|
package/package.json
CHANGED
|
@@ -884,44 +884,59 @@ readJson decoder ((Stream ( _, metadataDecoder ) _) as stream) =
|
|
|
884
884
|
, body = BackendTask.Http.jsonBody (pipelineEncoder stream "json")
|
|
885
885
|
, expect =
|
|
886
886
|
BackendTask.Http.expectJson
|
|
887
|
-
(Decode.
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
)
|
|
898
|
-
|
|
899
|
-
bodyResult
|
|
900
|
-
|> Decode.map
|
|
901
|
-
(\result ->
|
|
902
|
-
case result1 of
|
|
903
|
-
Ok metadata ->
|
|
904
|
-
case result of
|
|
905
|
-
Ok body ->
|
|
906
|
-
Ok
|
|
907
|
-
{ metadata = metadata
|
|
908
|
-
, body = body
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
Err decoderError ->
|
|
912
|
-
FatalError.recoverable
|
|
913
|
-
{ title = "Failed to decode body"
|
|
914
|
-
, body = "Failed to decode body"
|
|
915
|
-
}
|
|
916
|
-
(StreamError (Decode.errorToString decoderError))
|
|
917
|
-
|> Err
|
|
918
|
-
|
|
919
|
-
Err error ->
|
|
920
|
-
error
|
|
921
|
-
|> mapRecoverable (Result.toMaybe result)
|
|
922
|
-
|> Err
|
|
887
|
+
(Decode.oneOf
|
|
888
|
+
[ Decode.field "error" Decode.string
|
|
889
|
+
|> Decode.andThen
|
|
890
|
+
(\error ->
|
|
891
|
+
Decode.succeed
|
|
892
|
+
(Err
|
|
893
|
+
(FatalError.recoverable
|
|
894
|
+
{ title = "Stream Error"
|
|
895
|
+
, body = error
|
|
896
|
+
}
|
|
897
|
+
(StreamError error)
|
|
898
|
+
)
|
|
923
899
|
)
|
|
924
|
-
|
|
900
|
+
)
|
|
901
|
+
, Decode.field "metadata" metadataDecoder
|
|
902
|
+
|> Decode.andThen
|
|
903
|
+
(\result1 ->
|
|
904
|
+
let
|
|
905
|
+
bodyResult : Decoder (Result Decode.Error value)
|
|
906
|
+
bodyResult =
|
|
907
|
+
Decode.field "body" Decode.value
|
|
908
|
+
|> Decode.map
|
|
909
|
+
(\bodyValue ->
|
|
910
|
+
Decode.decodeValue decoder bodyValue
|
|
911
|
+
)
|
|
912
|
+
in
|
|
913
|
+
bodyResult
|
|
914
|
+
|> Decode.map
|
|
915
|
+
(\result ->
|
|
916
|
+
case result1 of
|
|
917
|
+
Ok metadata ->
|
|
918
|
+
case result of
|
|
919
|
+
Ok body ->
|
|
920
|
+
Ok
|
|
921
|
+
{ metadata = metadata
|
|
922
|
+
, body = body
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
Err decoderError ->
|
|
926
|
+
FatalError.recoverable
|
|
927
|
+
{ title = "Failed to decode body"
|
|
928
|
+
, body = "Failed to decode body"
|
|
929
|
+
}
|
|
930
|
+
(StreamError (Decode.errorToString decoderError))
|
|
931
|
+
|> Err
|
|
932
|
+
|
|
933
|
+
Err error ->
|
|
934
|
+
error
|
|
935
|
+
|> mapRecoverable (Result.toMaybe result)
|
|
936
|
+
|> Err
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
]
|
|
925
940
|
)
|
|
926
941
|
}
|
|
927
942
|
|> BackendTask.andThen BackendTask.fromResult
|
package/src/Pages/Script.elm
CHANGED
|
@@ -3,7 +3,7 @@ module Pages.Script exposing
|
|
|
3
3
|
, withCliOptions, withoutCliOptions
|
|
4
4
|
, writeFile
|
|
5
5
|
, command, exec
|
|
6
|
-
, log, sleep, doThen, which, expectWhich, question
|
|
6
|
+
, log, sleep, doThen, which, expectWhich, question, readKey, readKeyWithDefault
|
|
7
7
|
, Error(..)
|
|
8
8
|
)
|
|
9
9
|
|
|
@@ -31,7 +31,7 @@ Read more about using the `elm-pages` CLI to run (or bundle) scripts, plus a bri
|
|
|
31
31
|
|
|
32
32
|
## Utilities
|
|
33
33
|
|
|
34
|
-
@docs log, sleep, doThen, which, expectWhich, question
|
|
34
|
+
@docs log, sleep, doThen, which, expectWhich, question, readKey, readKeyWithDefault
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
## Errors
|
|
@@ -313,6 +313,77 @@ question prompt =
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
|
|
316
|
+
{-| Read a single keypress from stdin without requiring Enter.
|
|
317
|
+
|
|
318
|
+
This is useful for interactive prompts where you want immediate response
|
|
319
|
+
to a single key, like confirmation dialogs (y/n) or menu navigation.
|
|
320
|
+
|
|
321
|
+
module ConfirmDemo exposing (run)
|
|
322
|
+
|
|
323
|
+
import BackendTask
|
|
324
|
+
|
|
325
|
+
run : Script
|
|
326
|
+
run =
|
|
327
|
+
Script.withoutCliOptions
|
|
328
|
+
(Script.log "Approve this change? [y/n] "
|
|
329
|
+
|> BackendTask.andThen (\_ -> Script.readKey)
|
|
330
|
+
|> BackendTask.andThen
|
|
331
|
+
(\key ->
|
|
332
|
+
if key == "y" then
|
|
333
|
+
Script.log "Approved!"
|
|
334
|
+
|
|
335
|
+
else
|
|
336
|
+
Script.log "Rejected."
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
Note: Returns the raw key character. Control characters like Ctrl+C will
|
|
341
|
+
terminate the process.
|
|
342
|
+
|
|
343
|
+
When not running in an interactive terminal (e.g., piped input or CI),
|
|
344
|
+
falls back to line-buffered input and returns the first character of the line.
|
|
345
|
+
This allows scripts to work both interactively and with piped input like
|
|
346
|
+
`echo "y" | elm-pages run MyScript.elm`.
|
|
347
|
+
|
|
348
|
+
-}
|
|
349
|
+
readKey : BackendTask error String
|
|
350
|
+
readKey =
|
|
351
|
+
BackendTask.Internal.Request.request
|
|
352
|
+
{ body = BackendTask.Http.emptyBody
|
|
353
|
+
, expect = BackendTask.Http.expectJson Decode.string
|
|
354
|
+
, name = "readKey"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
{-| Like [`readKey`](#readKey), but returns a default value when Enter is pressed.
|
|
359
|
+
|
|
360
|
+
Script.log "Continue? [Y/n] "
|
|
361
|
+
|> BackendTask.andThen (\_ -> Script.readKeyWithDefault "y")
|
|
362
|
+
|> BackendTask.andThen
|
|
363
|
+
(\key ->
|
|
364
|
+
if String.toLower key == "y" then
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
else
|
|
368
|
+
abort
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
Useful for prompts where pressing Enter should accept a default option.
|
|
372
|
+
|
|
373
|
+
-}
|
|
374
|
+
readKeyWithDefault : String -> BackendTask error String
|
|
375
|
+
readKeyWithDefault default =
|
|
376
|
+
readKey
|
|
377
|
+
|> BackendTask.map
|
|
378
|
+
(\key ->
|
|
379
|
+
if key == "\u{000D}" || key == "\n" then
|
|
380
|
+
default
|
|
381
|
+
|
|
382
|
+
else
|
|
383
|
+
key
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
316
387
|
{-| Like [`command`](#command), but prints stderr and stdout to the console as the command runs instead of capturing them.
|
|
317
388
|
|
|
318
389
|
module MyScript exposing (run)
|