dxfl 0.1.7 → 0.1.8

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/.editorconfig ADDED
@@ -0,0 +1,22 @@
1
+ # Stop the editor from looking for .editorconfig files in the parent directories
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ # Preferred indentation size is a user preference
7
+ # tab_width = 4
8
+ end_of_line = lf
9
+ insert_final_newline = false
10
+ trim_trailing_whitespace = true
11
+ charset = utf-8
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
15
+
16
+ [*.{yml,yaml}]
17
+ indent_style = space
18
+ indent_size = 2
19
+
20
+ [*.json]
21
+ indent_style = space
22
+ indent_size = 2
@@ -0,0 +1,3 @@
1
+ # Ignore artifacts:
2
+ dist
3
+ node_modules
@@ -0,0 +1,3 @@
1
+ {
2
+ "arrowParens": "avoid"
3
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ - `deploy`: add support for redirects, configured from a new `deuxfleurs.toml` file.
2
+ - supports S3 "object redirects" (redirects from a path in the website)
3
+ - supports global "bucket redirects" (redirects from any path in the website
4
+ that starts with a specified prefix)
5
+ - allows specifying a default index page
6
+ - allows specifying a error page for 404 errors
7
+ - new `empty` command: removes all contents of a website (guichet requires emptying
8
+ a website before deleting it)
9
+ - `deploy`: instead of unconditionally deploying by default, ask for interactive
10
+ confirmation after displaying a summary of planned changes. Add a `--yes`
11
+ option to skip confirmation and automatically apply the changes.
12
+ - `deploy`: improve performance by increasing concurrency of S3 operations
13
+ - `deploy`: drop multipart upload support & simplify the strategy for
14
+ incremental uploads: simply use ETags now.
15
+
16
+ # v0.1.7
17
+
18
+ - internal packaging fixes
19
+
20
+ # v0.1.6
21
+
22
+ - improve the output of `deploy`
23
+ - show a full list of uploaded files, keeping multipart uploads on one line
24
+ - list deleted files
25
+ - show human-friendly file sizes instead of number of bytes
26
+ - `deploy`: add a `--dry-run` option to see planned changes without doing anything
27
+
28
+ # v0.1.5
29
+
30
+ - make `deploy` incremental: only upload files that have changed.
31
+ The implementation strategy is by the one used in rclone: store the file md5
32
+ in a custom metadata header.
33
+ - fix a crash in case of empty ListObject reply
34
+
35
+ # v0.1.3, v0.1.4
36
+
37
+ - misc internal tooling changes
38
+
39
+ # v0.1.1, v0.1.2
40
+
41
+ - rename the tool to `dxfl`
42
+
43
+ # v0.1.0
44
+
45
+ - new `login` command: authenticates to guichet & stores credentials locally
46
+ - new `list` command: list websites of user (after logging in)
47
+ - new `deploy` command: deploys a local directory to a user's website.
48
+ Also detects the MIME type of files and sets ContentType accordingly.
package/README.md CHANGED
@@ -10,7 +10,7 @@ npm install -g dxfl
10
10
 
11
11
  ## Usage
12
12
 
13
- *Not ready*
13
+ _Not ready_
14
14
 
15
15
  Start by login with your username, for example for `john`:
16
16
 
@@ -30,6 +30,56 @@ And then to deploy your `_public` folder on `example.com`:
30
30
  dxfl deploy example.com _public
31
31
  ```
32
32
 
33
+ Or potentially preview the upcoming changes with the `--dry-run` option:
34
+
35
+ ```
36
+ dxfl deploy example.com _public --dry-run
37
+ ```
38
+
39
+ If you need to empty a website from its contents:
40
+
41
+ ```
42
+ dxfl empty example.com
43
+ ```
44
+
45
+ ## Website configuration
46
+
47
+ `dxfl deploy` reads a `deuxfleurs.toml` configuration file (if it exists in the current directory).
48
+ This file can be used to specify website configuration metadata, such as redirections.
49
+
50
+ Your `deuxfleurs.toml` should follow the following structure:
51
+
52
+ ```toml
53
+ # Filename added after URLs that point to directories.
54
+ # Default value: "index.html"
55
+ index_page = "index.html"
56
+ # Path to the file to serve in case of 404 error
57
+ # Default value: none.
58
+ error_page = "404.html"
59
+
60
+ # A redirect entry. There can be as many as desired.
61
+ [[redirects]]
62
+ # Source of the redirection.
63
+ from = "old/path"
64
+ # Target of the redirection.
65
+ to = "new/path"
66
+
67
+ # Redirects can also match multiple paths, by specifying
68
+ # a prefix followed by *
69
+ [[redirects]]
70
+ # Source of the redirection: all paths that match the prefix
71
+ # foobar/
72
+ from = "foobar/*"
73
+ # Target: redirect all foobar/XXX to baz/XXX.
74
+ # The target can also be an URL outside of the website, or a
75
+ # single path.
76
+ to = "baz/*"
77
+ # Optional: custom HTTP status code for the redirection.
78
+ status = 302
79
+ # Optional: only redirect if the original request triggered a 404 error
80
+ if_error = 404
81
+ ```
82
+
33
83
  ## Development
34
84
 
35
85
  ```
@@ -43,6 +93,18 @@ dxfl
43
93
  npx dxfl
44
94
  ```
45
95
 
96
+ ### Code formatting
97
+
98
+ [Prettier](https://prettier.io/) is used to assure a certain consistency in style (and [accessibility](https://adamtuttle.codes/blog/2021/tabs-vs-spaces-its-an-accessibility-issue/)) through the codebase. An [EditorConfig](https://editorconfig.org/) file is also here with a similar goal.
99
+
100
+ You can format your changes with the dedicated npm command lines:
101
+
102
+ ```bash
103
+ npm run prettier-check # check formatting without making change
104
+ npm run prettier # fix formatting
105
+ npm run prettier-watch # watch upcoming changes to fix
106
+ ```
107
+
46
108
  ## Release
47
109
 
48
110
  First you need an account on npmjs.com and be a maintainer of the `dxfl` package (ask quentin).
@@ -54,6 +116,7 @@ Then to publish a release:
54
116
  vim package.json # update the version in this file
55
117
  vim index.ts # update the version in this file
56
118
  npm install # update the version in the package-lock.json
119
+ npm run prettier # fix potential coding style problem
57
120
  git commit -a -m 'set version 0.1.5' # commit your change
58
121
  git push # send update
59
122
  git tag -m 'v0.1.5' v0.1.5 # create associated tag
package/dist/_empty.js ADDED
@@ -0,0 +1,64 @@
1
+ // // TODO: Refacto the codebase global before integrate this feature
2
+ export {};
3
+ // export async function empty(
4
+ // vhost: string,
5
+ // options: { dryRun: boolean | undefined },
6
+ // ) {
7
+ // const conf = await openApiConf();
8
+ // // Get website info from guichet (bucket name and keys)
9
+ // const api = new WebsiteApi(conf);
10
+ // let vhostInfo = await api.getWebsite({ vhost }).catch(err => {
11
+ // if (err.response.status == 404) {
12
+ // console.error(`Error: website '${vhost}' does not exist`);
13
+ // } else {
14
+ // console.error(err);
15
+ // }
16
+ // process.exit(1);
17
+ // });
18
+ // // List the files currently stored in the bucket
19
+ // // @FIXME this info could be returned by the guichet API
20
+ // const s3client = new S3Client({
21
+ // endpoint: "https://garage.deuxfleurs.fr",
22
+ // region: "garage",
23
+ // forcePathStyle: true,
24
+ // credentials: {
25
+ // accessKeyId: vhostInfo.accessKeyId!,
26
+ // secretAccessKey: vhostInfo.secretAccessKey!,
27
+ // },
28
+ // });
29
+ // const Bucket = vhostInfo.vhost!.name!;
30
+ // const filesToDelete = [...(await getBucketFiles(s3client, Bucket))].map(
31
+ // ([name, { size }]) => ({
32
+ // name,
33
+ // size,
34
+ // }),
35
+ // );
36
+ // for (const file of filesToDelete) {
37
+ // process.stdout.write(`Deleting ${file.name}\n`);
38
+ // }
39
+ // // If not in dry-run mode, send the delete command
40
+ // if (!options.dryRun) {
41
+ // const resp = await deleteFiles(s3client, Bucket, filesToDelete);
42
+ // if (resp && resp!.$metadata.httpStatusCode != 200) {
43
+ // // TODO: better error handling?
44
+ // console.error(resp);
45
+ // process.exit(1);
46
+ // }
47
+ // }
48
+ // // Display a summary
49
+ // function sum(a: number[]) {
50
+ // return a.reduce((x, y) => x + y, 0);
51
+ // }
52
+ // function formatFiles(n: number) {
53
+ // if (n == 1) {
54
+ // return `${n} file `;
55
+ // } else {
56
+ // return `${n} files`;
57
+ // }
58
+ // }
59
+ // const sizeDeleted = sum(filesToDelete.map(f => f.size ?? 0));
60
+ // process.stdout.write("\nSummary:\n");
61
+ // process.stdout.write(
62
+ // `${formatFiles(filesToDelete.length)} deleted (${formatBytes(sizeDeleted)})\n`,
63
+ // );
64
+ // }
package/dist/auth.js CHANGED
@@ -8,9 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { Configuration, WebsiteApi } from "guichet-sdk-ts";
11
- import { read } from 'read';
12
- import path from 'node:path';
13
- import fs from 'node:fs/promises';
11
+ import { read } from "read";
12
+ import path from "node:path";
13
+ import fs from "node:fs/promises";
14
14
  function configPath() {
15
15
  let path = ".dfl/config.json";
16
16
  if (process.env.XDG_CONFIG_HOME) {
@@ -27,7 +27,7 @@ export function openApiConf() {
27
27
  let dictConf;
28
28
  const configFile = configPath();
29
29
  try {
30
- strConf = yield fs.readFile(configFile, { encoding: 'utf8' });
30
+ strConf = yield fs.readFile(configFile, { encoding: "utf8" });
31
31
  }
32
32
  catch (err) {
33
33
  console.error(err, `\n\nUnable to read ${configFile}, run 'dfl login' first.`);
@@ -52,7 +52,7 @@ export function login(username) {
52
52
  const password = yield read({
53
53
  prompt: "password: ",
54
54
  silent: true,
55
- replace: "*"
55
+ replace: "*",
56
56
  });
57
57
  // check config
58
58
  const testConf = new Configuration({
@@ -76,6 +76,6 @@ export function login(username) {
76
76
  const serializedConfig = JSON.stringify(configData);
77
77
  yield fs.writeFile(configFile, serializedConfig, { mode: 0o600 });
78
78
  // @FIXME: we would like to avoid storing the password in clear text in the future.
79
- console.log('ok');
79
+ console.log("ok");
80
80
  });
81
81
  }
package/dist/bucket.js ADDED
@@ -0,0 +1,174 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import fs from "fs";
11
+ import mime from "mime";
12
+ import { DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
13
+ import { PromisePool } from "@supercharge/promise-pool";
14
+ import { parseEtag, toChunks, formatBytesHuman } from "./utils.js";
15
+ export function getBucket(api, vhost) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ // Get website info from guichet (bucket name and keys)
18
+ const bucket = yield api.getWebsite({ vhost }).catch(err => {
19
+ var _a;
20
+ if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) == 404) {
21
+ console.error(`Error: website '${vhost}' does not exist`);
22
+ }
23
+ else {
24
+ console.error(err);
25
+ }
26
+ throw new Error(err);
27
+ });
28
+ // Initialize a S3 Client
29
+ const client = new S3Client({
30
+ endpoint: "https://garage.deuxfleurs.fr",
31
+ region: "garage",
32
+ forcePathStyle: true,
33
+ credentials: {
34
+ accessKeyId: bucket.accessKeyId,
35
+ secretAccessKey: bucket.secretAccessKey,
36
+ },
37
+ });
38
+ return {
39
+ client: client,
40
+ name: bucket.vhost.name,
41
+ };
42
+ });
43
+ }
44
+ export function getBucketFiles(bucket) {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ const files = new Map();
47
+ let done = false;
48
+ let cmd = new ListObjectsV2Command({ Bucket: bucket.name });
49
+ while (!done) {
50
+ const resp = yield bucket.client.send(cmd);
51
+ if (resp.$metadata.httpStatusCode != 200) {
52
+ // TODO: better error handling?
53
+ throw resp;
54
+ }
55
+ if (resp.Contents) {
56
+ for (const item of resp.Contents) {
57
+ const etag = item.ETag ? parseEtag(item.ETag) : undefined;
58
+ if (item.Key) {
59
+ files.set(item.Key, { size: item.Size, etag });
60
+ }
61
+ }
62
+ }
63
+ if (resp.NextContinuationToken) {
64
+ cmd = new ListObjectsV2Command({
65
+ Bucket: bucket.name,
66
+ ContinuationToken: resp.NextContinuationToken,
67
+ });
68
+ }
69
+ else {
70
+ done = true;
71
+ }
72
+ }
73
+ return files;
74
+ });
75
+ }
76
+ export function getBucketFilesDetails(bucket, files) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ let res = new Map();
79
+ function doFile(file) {
80
+ return __awaiter(this, void 0, void 0, function* () {
81
+ const resp = yield bucket.client.send(new HeadObjectCommand({ Bucket: bucket.name, Key: file }));
82
+ if (resp.$metadata.httpStatusCode != 200) {
83
+ // TODO: better error handling?
84
+ throw resp;
85
+ }
86
+ res.set(file, {
87
+ redirect: resp.WebsiteRedirectLocation,
88
+ });
89
+ });
90
+ }
91
+ yield PromisePool.for(files)
92
+ .withConcurrency(50)
93
+ .handleError(err => {
94
+ throw err;
95
+ })
96
+ .process(doFile);
97
+ return res;
98
+ });
99
+ }
100
+ export function deleteBucketFile(bucket, name) {
101
+ return __awaiter(this, void 0, void 0, function* () {
102
+ const resp = yield bucket.client.send(new DeleteObjectCommand({
103
+ Bucket: bucket.name,
104
+ Key: name,
105
+ }));
106
+ if (resp && resp.$metadata.httpStatusCode != 204) {
107
+ // TODO: better error handling?
108
+ throw resp;
109
+ }
110
+ });
111
+ }
112
+ export function deleteBucketFiles(bucket, files) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ const chunks = toChunks(files, 20);
115
+ yield PromisePool.for(chunks)
116
+ .withConcurrency(50)
117
+ .handleError(err => {
118
+ throw err;
119
+ })
120
+ .process((files) => __awaiter(this, void 0, void 0, function* () {
121
+ for (const file of files) {
122
+ process.stdout.write(` Delete ${file.name} (${file.size ? formatBytesHuman(file.size) : "?B"})\n`);
123
+ }
124
+ const resp = yield bucket.client.send(new DeleteObjectsCommand({
125
+ Bucket: bucket.name,
126
+ Delete: {
127
+ Objects: files.map(f => {
128
+ return { Key: f.name };
129
+ }),
130
+ },
131
+ }));
132
+ if (resp && resp.$metadata.httpStatusCode != 200) {
133
+ // TODO: better error handling?
134
+ throw resp;
135
+ }
136
+ }));
137
+ });
138
+ }
139
+ export function uploadFile(bucket, s3Path, localPath) {
140
+ return __awaiter(this, void 0, void 0, function* () {
141
+ var _a;
142
+ // use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
143
+ let ContentType = (_a = mime.getType(localPath)) !== null && _a !== void 0 ? _a : undefined;
144
+ // add charset=utf-8 by default on text files (TODO: allow the user to override this)
145
+ if (ContentType && ContentType.startsWith("text/")) {
146
+ ContentType = ContentType + "; charset=utf-8";
147
+ }
148
+ const Body = fs.createReadStream(localPath);
149
+ const params = { Bucket: bucket.name, Key: s3Path, Body, ContentType };
150
+ const resp = yield bucket.client.send(new PutObjectCommand(params));
151
+ if (resp && resp.$metadata.httpStatusCode != 200) {
152
+ throw resp;
153
+ }
154
+ });
155
+ }
156
+ // the 'source' object may or may not exist already in the bucket.
157
+ // if it exists, its contents will be discarded, otherwise it
158
+ // will be created with empty contents.
159
+ export function putEmptyObjectRedirect(bucket, source, target) {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ // use a non-empty Body to sidestep
162
+ // https://github.com/aws/aws-sdk-js-v3/issues/5722
163
+ const params = {
164
+ Bucket: bucket.name,
165
+ Key: source,
166
+ Body: "source of object redirect",
167
+ WebsiteRedirectLocation: target,
168
+ };
169
+ const resp = yield bucket.client.send(new PutObjectCommand(params));
170
+ if (resp && resp.$metadata.httpStatusCode != 200) {
171
+ throw resp;
172
+ }
173
+ });
174
+ }
package/dist/config.js ADDED
@@ -0,0 +1,185 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import fs from "fs";
11
+ import TOML from "smol-toml";
12
+ import URI from "fast-uri";
13
+ import { z as zod } from "zod";
14
+ import { fromError as zodError } from "zod-validation-error";
15
+ const DXFL_CONFIG = "deuxfleurs.toml";
16
+ // Parsing: TOML -> untyped object
17
+ function readConfigFile() {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ if (!fs.existsSync(DXFL_CONFIG)) {
20
+ return {};
21
+ }
22
+ let strConf;
23
+ let dictConf;
24
+ try {
25
+ strConf = yield fs.promises.readFile(DXFL_CONFIG, { encoding: "utf8" });
26
+ }
27
+ catch (err) {
28
+ console.error(err, `\n\nUnable to read ${DXFL_CONFIG}`);
29
+ process.exit(1);
30
+ }
31
+ try {
32
+ dictConf = TOML.parse(strConf);
33
+ }
34
+ catch (err) {
35
+ console.error(err, `\n\nUnable to parse ${DXFL_CONFIG} as TOML, please check it for syntax errors.`);
36
+ process.exit(1);
37
+ }
38
+ return dictConf;
39
+ });
40
+ }
41
+ // Parsing: untyped object -> RawConfig
42
+ const RawRedirectSchema = zod.object({
43
+ from: zod.string(),
44
+ to: zod.string(),
45
+ if_error: zod.number().int().optional(),
46
+ code: zod.number().int().positive().optional(),
47
+ });
48
+ const RawConfigSchema = zod.object({
49
+ index_page: zod.string().optional(),
50
+ error_page: zod.string().optional(),
51
+ redirects: zod.array(RawRedirectSchema),
52
+ });
53
+ function interpRawConfig(cfg) {
54
+ try {
55
+ return RawConfigSchema.parse(cfg);
56
+ }
57
+ catch (err) {
58
+ const validationError = zodError(err);
59
+ throw validationError.toString();
60
+ }
61
+ }
62
+ // Parsing: RawConfig -> DxflConfig
63
+ function unescape(s) {
64
+ let out = "";
65
+ const iter = s[Symbol.iterator]();
66
+ while (true) {
67
+ const elt = iter.next();
68
+ if (elt.done == true) {
69
+ return { s: out, is_prefix: false };
70
+ }
71
+ if (elt.value == "*") {
72
+ const next = iter.next();
73
+ if (next.done == true) {
74
+ return { s: out, is_prefix: true };
75
+ }
76
+ else if (next.value == "*") {
77
+ out += "*";
78
+ }
79
+ else {
80
+ throw `a single '*' is only allowed at the end for wildcards; please use '**' instead`;
81
+ }
82
+ }
83
+ else {
84
+ out += elt.value;
85
+ }
86
+ }
87
+ }
88
+ function interpConfig(rawcfg) {
89
+ function interpPath(name, str) {
90
+ try {
91
+ return unescape(str);
92
+ }
93
+ catch (err) {
94
+ throw `${name}: ${err}`;
95
+ }
96
+ }
97
+ function interpRedirect(i, r) {
98
+ const rfrom = interpPath(`redirect ${i}, from`, r.from);
99
+ const rto = interpPath(`redirect ${i}, to`, r.to);
100
+ let redirect;
101
+ if (rfrom.is_prefix) {
102
+ // This is a bucket redirect
103
+ const toURI = URI.parse(rto.s);
104
+ if (toURI.error) {
105
+ throw `redirect ${i}, 'to': ${toURI.error}`;
106
+ }
107
+ if (toURI.scheme && toURI.scheme != "http" && toURI.scheme != "https") {
108
+ throw `redirect ${i}, 'to': unsupported URI scheme ${toURI.scheme}; http or https required`;
109
+ }
110
+ if (toURI.port) {
111
+ throw `redirect ${i}, 'to': specifying a port is not supported`;
112
+ }
113
+ if (toURI.query) {
114
+ throw `redirect ${i}, 'to': specifying a query is not supported`;
115
+ }
116
+ let bredirect = {
117
+ prefix: rfrom.s,
118
+ if_error: undefined,
119
+ hostname: toURI.host,
120
+ protocol: toURI.scheme,
121
+ code: undefined,
122
+ to: rto.is_prefix
123
+ ? { kind: "replace_prefix", prefix: rto.s }
124
+ : { kind: "replace", target: rto.s },
125
+ };
126
+ if (r.if_error) {
127
+ if (r.if_error != 404) {
128
+ throw `redirect ${i}: the only currently supportetd value for 'if_error' is 404`;
129
+ }
130
+ bredirect.if_error = 404;
131
+ }
132
+ if (r.code) {
133
+ if ([301, 302, 303, 307, 308].includes(r.code)) {
134
+ bredirect.code = r.code;
135
+ }
136
+ else if ([200, 404].includes(r.code)) {
137
+ if (toURI.host || toURI.scheme) {
138
+ throw `request ${i}: a code of 200 or 404 is not accepted while specifying a protocol and hostname in the destination`;
139
+ }
140
+ bredirect.code = r.code;
141
+ }
142
+ else {
143
+ throw `redirect ${i}: unsupported error code ${r.code}`;
144
+ }
145
+ }
146
+ redirect = { kind: "bucket", r: bredirect };
147
+ }
148
+ else {
149
+ // This is an object redirect
150
+ if (r.if_error) {
151
+ throw `redirect ${i}: 'if_error' is not supported for single redirections`;
152
+ }
153
+ if (r.code) {
154
+ throw `redirect ${i}: 'code' is not supported for single redirections`;
155
+ }
156
+ if (rto.is_prefix) {
157
+ throw `redirect ${i}: cannot use a prefix target in 'to' when 'from' is a single address`;
158
+ }
159
+ // pass the 'to' field as-is
160
+ const oredirect = { from: rfrom.s, to: rto.s };
161
+ redirect = { kind: "object", r: oredirect };
162
+ }
163
+ return redirect;
164
+ }
165
+ let cfg = {
166
+ index_page: rawcfg.index_page,
167
+ error_page: rawcfg.error_page,
168
+ redirects: [],
169
+ };
170
+ for (const [i, r] of rawcfg.redirects.entries()) {
171
+ cfg.redirects.push(interpRedirect(i, r));
172
+ }
173
+ return cfg;
174
+ }
175
+ export function readConfig() {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ try {
178
+ return interpConfig(interpRawConfig(yield readConfigFile()));
179
+ }
180
+ catch (err) {
181
+ console.error(`Error while reading ${DXFL_CONFIG}: ${err}`);
182
+ process.exit(1);
183
+ }
184
+ });
185
+ }