alchemy 2.0.0-beta.2 → 2.0.0-beta.4
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/bin/{alchemy-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Provider from "../Provider.ts";
|
|
4
|
+
import { Resource } from "../Resource.ts";
|
|
5
|
+
|
|
6
|
+
export interface VariableProps {
|
|
7
|
+
/**
|
|
8
|
+
* Repository owner (user or organization).
|
|
9
|
+
*/
|
|
10
|
+
owner: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Repository name.
|
|
14
|
+
*/
|
|
15
|
+
repository: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Variable name (e.g. `AWS_ROLE_ARN`).
|
|
19
|
+
*/
|
|
20
|
+
name: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Variable value.
|
|
24
|
+
*/
|
|
25
|
+
value: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* GitHub API token. If not provided, falls back to
|
|
29
|
+
* `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
|
|
30
|
+
*/
|
|
31
|
+
token?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Variable extends Resource<
|
|
35
|
+
"GitHub.Variable",
|
|
36
|
+
VariableProps,
|
|
37
|
+
{
|
|
38
|
+
/**
|
|
39
|
+
* ISO-8601 timestamp of the last update.
|
|
40
|
+
*/
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
}
|
|
43
|
+
> {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A GitHub Actions repository variable.
|
|
47
|
+
*
|
|
48
|
+
* `Variable` manages the lifecycle of a plain-text configuration variable
|
|
49
|
+
* in GitHub Actions. Variables are visible in workflow logs and are
|
|
50
|
+
* suitable for non-sensitive configuration like region names, environment
|
|
51
|
+
* labels, or feature flags. For sensitive values, use `GitHub.Secret`
|
|
52
|
+
* instead.
|
|
53
|
+
*
|
|
54
|
+
* Authentication is resolved in order: explicit `token` prop,
|
|
55
|
+
* `GITHUB_ACCESS_TOKEN` env var, `GITHUB_TOKEN` env var. The token needs
|
|
56
|
+
* `repo` scope for private repositories or `public_repo` for public ones.
|
|
57
|
+
*
|
|
58
|
+
* @section Repository Variables
|
|
59
|
+
* Store variables accessible to all GitHub Actions workflows in the
|
|
60
|
+
* repository.
|
|
61
|
+
*
|
|
62
|
+
* @example Create a Repository Variable
|
|
63
|
+
* ```typescript
|
|
64
|
+
* yield* GitHub.Variable("aws-region", {
|
|
65
|
+
* owner: "my-org",
|
|
66
|
+
* repository: "my-repo",
|
|
67
|
+
* name: "AWS_REGION",
|
|
68
|
+
* value: "us-east-1",
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @section Wiring with Other Resources
|
|
73
|
+
* Pass output attributes from other resources into GitHub variables so
|
|
74
|
+
* that CI workflows can reference them.
|
|
75
|
+
*
|
|
76
|
+
* @example Store a Worker URL for CI
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const worker = yield* Cloudflare.Worker("Api", { ... });
|
|
79
|
+
*
|
|
80
|
+
* yield* GitHub.Variable("api-url", {
|
|
81
|
+
* owner: "my-org",
|
|
82
|
+
* repository: "my-repo",
|
|
83
|
+
* name: "API_URL",
|
|
84
|
+
* value: worker.url!,
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example Multiple Variables
|
|
89
|
+
* ```typescript
|
|
90
|
+
* yield* GitHub.Variable("region", {
|
|
91
|
+
* owner: "my-org",
|
|
92
|
+
* repository: "my-repo",
|
|
93
|
+
* name: "AWS_REGION",
|
|
94
|
+
* value: "us-east-1",
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* yield* GitHub.Variable("stage", {
|
|
98
|
+
* owner: "my-org",
|
|
99
|
+
* repository: "my-repo",
|
|
100
|
+
* name: "DEPLOY_STAGE",
|
|
101
|
+
* value: "production",
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const Variable = Resource<Variable>("GitHub.Variable");
|
|
106
|
+
|
|
107
|
+
function resolveToken(props: VariableProps): string | undefined {
|
|
108
|
+
return (
|
|
109
|
+
props.token ?? process.env.GITHUB_ACCESS_TOKEN ?? process.env.GITHUB_TOKEN
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createClient(props: VariableProps): Octokit {
|
|
114
|
+
return new Octokit({ auth: resolveToken(props) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const VariableProvider = () =>
|
|
118
|
+
Provider.succeed(Variable, {
|
|
119
|
+
create: Effect.fn(function* ({ news }) {
|
|
120
|
+
const octokit = createClient(news);
|
|
121
|
+
|
|
122
|
+
yield* Effect.tryPromise(() =>
|
|
123
|
+
octokit.rest.actions.createRepoVariable({
|
|
124
|
+
owner: news.owner,
|
|
125
|
+
repo: news.repository,
|
|
126
|
+
name: news.name,
|
|
127
|
+
value: news.value,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { updatedAt: new Date().toISOString() };
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
update: Effect.fn(function* ({ news }) {
|
|
135
|
+
const octokit = createClient(news);
|
|
136
|
+
|
|
137
|
+
yield* Effect.tryPromise(() =>
|
|
138
|
+
octokit.rest.actions.updateRepoVariable({
|
|
139
|
+
owner: news.owner,
|
|
140
|
+
repo: news.repository,
|
|
141
|
+
name: news.name,
|
|
142
|
+
value: news.value,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return { updatedAt: new Date().toISOString() };
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
delete: Effect.fn(function* ({ olds }) {
|
|
150
|
+
const octokit = createClient(olds);
|
|
151
|
+
|
|
152
|
+
yield* Effect.tryPromise(async () => {
|
|
153
|
+
try {
|
|
154
|
+
await octokit.rest.actions.deleteRepoVariable({
|
|
155
|
+
owner: olds.owner,
|
|
156
|
+
repo: olds.repository,
|
|
157
|
+
name: olds.name,
|
|
158
|
+
});
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
if (error.status !== 404) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}),
|
|
166
|
+
});
|
package/src/Kubernetes/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AwsClient } from "aws4fetch";
|
|
2
1
|
import { Credentials } from "@distilled.cloud/aws/Credentials";
|
|
3
2
|
import { Region } from "@distilled.cloud/aws/Region";
|
|
3
|
+
import { AwsClient } from "aws4fetch";
|
|
4
4
|
import * as Data from "effect/Data";
|
|
5
5
|
import * as Effect from "effect/Effect";
|
|
6
6
|
import * as Redacted from "effect/Redacted";
|
|
@@ -28,7 +28,7 @@ export interface KubernetesClusterConnection {
|
|
|
28
28
|
certificateAuthorityData: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const fieldManager = "alchemy
|
|
31
|
+
const fieldManager = "alchemy";
|
|
32
32
|
|
|
33
33
|
const createBearerToken = Effect.fn(function* (clusterName: string) {
|
|
34
34
|
const credentials = yield* yield* Credentials;
|
package/src/Output.ts
CHANGED
|
@@ -90,7 +90,7 @@ export abstract class BaseExpr<A = any, Req = any> implements Output<A, Req> {
|
|
|
90
90
|
declare readonly A: A;
|
|
91
91
|
declare readonly src: ResourceLike;
|
|
92
92
|
declare readonly req: Req;
|
|
93
|
-
// we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy
|
|
93
|
+
// we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy module imports
|
|
94
94
|
constructor() {}
|
|
95
95
|
as<T>(): Output<T, Req> {
|
|
96
96
|
return this as any;
|
package/src/Platform.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type Main<Services = never> = void | {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export type Rpc<Shape> = {
|
|
36
|
-
"~alchemy
|
|
36
|
+
"~alchemy/rpc": Shape;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
// services provided to the Resource
|
|
@@ -220,10 +220,15 @@ export const Platform = <
|
|
|
220
220
|
// return new Response("Hello, world!");
|
|
221
221
|
// }
|
|
222
222
|
// }
|
|
223
|
-
resource(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
resource(
|
|
224
|
+
id,
|
|
225
|
+
Effect.isEffect(props)
|
|
226
|
+
? Effect.map(props, (p: any) => ({ ...p, isExternal: true }))
|
|
227
|
+
: {
|
|
228
|
+
...props,
|
|
229
|
+
isExternal: true,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
227
232
|
: Effect.flatMap(
|
|
228
233
|
// this is a tagged resource
|
|
229
234
|
Effect.serviceOption(cls.Self),
|
package/src/Provider.ts
CHANGED
|
@@ -369,7 +369,7 @@ export const tryFindProviderByType: {
|
|
|
369
369
|
const Tag = Provider<R>(resourceType) as Context.Service<Provider<R>, any>;
|
|
370
370
|
const direct = yield* Effect.serviceOption(Tag);
|
|
371
371
|
if (Option.isSome(direct)) {
|
|
372
|
-
return direct
|
|
372
|
+
return direct;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
const context = yield* Effect.context<never>();
|
package/src/Resource.ts
CHANGED
|
@@ -31,10 +31,10 @@ export type ResourceConstructor<R extends ResourceLike, Req = never> = {
|
|
|
31
31
|
},
|
|
32
32
|
]
|
|
33
33
|
): Effect.Effect<R, never, Req>;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
<PropsReq = never>(
|
|
35
|
+
id: string,
|
|
36
|
+
props: Effect.Effect<Input<R["Props"]>, never, PropsReq>,
|
|
37
|
+
): Effect.Effect<R, never, PropsReq | Req>;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export type ResourceClassWithMethods<
|
package/src/Test/Vitest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as aws from "@distilled.cloud/aws";
|
|
2
2
|
import * as cf from "@distilled.cloud/cloudflare";
|
|
3
|
-
import { NodeServices } from "@effect/platform-node";
|
|
3
|
+
// import { NodeServices } from "@effect/platform-node";
|
|
4
4
|
import { expect, it } from "@effect/vitest";
|
|
5
5
|
import { Logger } from "effect";
|
|
6
6
|
import * as Config from "effect/Config";
|
|
@@ -44,6 +44,7 @@ import * as Serverless from "../Serverless/index.ts";
|
|
|
44
44
|
import * as Stack from "../Stack.ts";
|
|
45
45
|
import * as Stage from "../Stage.ts";
|
|
46
46
|
import * as State from "../State/index.ts";
|
|
47
|
+
import { PlatformServices } from "../Util/PlatformServices.ts";
|
|
47
48
|
import { TestCli } from "./TestCli.ts";
|
|
48
49
|
|
|
49
50
|
export const expectEmptyObject = () =>
|
|
@@ -90,7 +91,7 @@ const quietLogger = Logger.make(() => {
|
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
const platform = Layer.mergeAll(
|
|
93
|
-
|
|
94
|
+
PlatformServices,
|
|
94
95
|
FetchHttpClient.layer,
|
|
95
96
|
Logger.layer([process.env.VERBOSE ? Logger.consolePretty() : quietLogger]),
|
|
96
97
|
);
|
|
@@ -212,7 +213,7 @@ const runWithContext = <A, Err>(
|
|
|
212
213
|
MinimumLogLevel,
|
|
213
214
|
process.env.DEBUG ? "Debug" : "Info",
|
|
214
215
|
),
|
|
215
|
-
Effect.provide(
|
|
216
|
+
Effect.provide(PlatformServices),
|
|
216
217
|
);
|
|
217
218
|
};
|
|
218
219
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
5
|
+
|
|
6
|
+
export class PromptCancelled extends Data.TaggedError("PromptCancelled") {}
|
|
7
|
+
export const retryIfCancelled = Effect.retry({
|
|
8
|
+
while: (e: unknown) => e instanceof PromptCancelled,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a clack prompt (which returns a `Promise<T | symbol>` where the
|
|
13
|
+
* symbol indicates user cancellation) in an Effect.
|
|
14
|
+
*
|
|
15
|
+
* Returns `undefined` if the user cancels (Ctrl+C / Escape).
|
|
16
|
+
*
|
|
17
|
+
* Uses `Effect.callback` so fiber interruption propagates via the abort
|
|
18
|
+
* signal to any async resources we own; the clack prompt itself is left
|
|
19
|
+
* to resolve — its result is ignored after interruption.
|
|
20
|
+
*/
|
|
21
|
+
export const prompt = <T>(
|
|
22
|
+
fn: () => Promise<T | symbol>,
|
|
23
|
+
): Effect.Effect<T, PromptCancelled> =>
|
|
24
|
+
Effect.callback<T, PromptCancelled>((resume, signal) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
fn().then(
|
|
27
|
+
(result) => {
|
|
28
|
+
if (settled || signal.aborted) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
if (p.isCancel(result)) {
|
|
31
|
+
resume(Effect.fail(new PromptCancelled()));
|
|
32
|
+
} else {
|
|
33
|
+
resume(Effect.succeed(result as T));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
(err) => {
|
|
37
|
+
if (settled || signal.aborted) return;
|
|
38
|
+
settled = true;
|
|
39
|
+
resume(Effect.die(err));
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const success = (str: string) => Effect.sync(() => p.log.success(str));
|
|
45
|
+
export const warn = (str: string) => Effect.sync(() => p.log.warn(str));
|
|
46
|
+
export const error = (str: string) => Effect.sync(() => p.log.error(str));
|
|
47
|
+
export const info = (str: string) => Effect.sync(() => p.log.info(str));
|
|
48
|
+
export const text = (opts: p.TextOptions) => prompt(() => p.text(opts));
|
|
49
|
+
export const password = (opts: p.PasswordOptions) =>
|
|
50
|
+
prompt(() => p.password(opts));
|
|
51
|
+
export const select = <Value>(opts: p.SelectOptions<Value>) =>
|
|
52
|
+
prompt(() => p.select<Value>(opts));
|
|
53
|
+
export const confirm = (opts: p.ConfirmOptions) =>
|
|
54
|
+
prompt(() => p.confirm(opts));
|
|
55
|
+
export const multiselect = <Value>(opts: p.MultiSelectOptions<Value>) =>
|
|
56
|
+
prompt(() => p.multiselect<Value>(opts));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Open a URL in the user's default browser.
|
|
60
|
+
*
|
|
61
|
+
* On Windows, uses rundll32's FileProtocolHandler — a built-in shim that
|
|
62
|
+
* opens URLs in the default browser. It accepts the URL as a direct
|
|
63
|
+
* argument (no shell, no quoting of `&`). cmd.exe `start` would treat
|
|
64
|
+
* `&` in OAuth URLs as a command separator, and `explorer.exe` treats
|
|
65
|
+
* its arg as a path.
|
|
66
|
+
*/
|
|
67
|
+
export const openUrl = (url: string) =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const [cmd, args] =
|
|
70
|
+
process.platform === "win32"
|
|
71
|
+
? (["rundll32.exe", ["url.dll,FileProtocolHandler", url]] as const)
|
|
72
|
+
: process.platform === "darwin"
|
|
73
|
+
? (["open", [url]] as const)
|
|
74
|
+
: (["xdg-open", [url]] as const);
|
|
75
|
+
const handle = yield* ChildProcess.make(cmd, [...args], { shell: false });
|
|
76
|
+
yield* handle.exitCode;
|
|
77
|
+
}).pipe(Effect.scoped);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BunServices } from "@effect/platform-bun/BunServices";
|
|
2
|
+
import type { NodeServices } from "@effect/platform-node/NodeServices";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
|
|
6
|
+
const isBun = typeof Bun !== "undefined";
|
|
7
|
+
|
|
8
|
+
export const PlatformServices: Layer.Layer<
|
|
9
|
+
NodeServices | BunServices,
|
|
10
|
+
never,
|
|
11
|
+
never
|
|
12
|
+
> = Effect.promise(() => {
|
|
13
|
+
if (isBun) {
|
|
14
|
+
return import("@effect/platform-bun").then(
|
|
15
|
+
(platform) => platform.BunServices.layer,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return import("@effect/platform-node").then(
|
|
19
|
+
(platform) => platform.NodeServices.layer,
|
|
20
|
+
);
|
|
21
|
+
}).pipe(Layer.unwrap);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagged template literal for removing indentation from a block of text.
|
|
3
|
+
*
|
|
4
|
+
* If the first line is empty, it will be ignored.
|
|
5
|
+
*/
|
|
6
|
+
export function dedent(
|
|
7
|
+
strings: TemplateStringsArray,
|
|
8
|
+
...values: unknown[]
|
|
9
|
+
): string;
|
|
10
|
+
export function dedent(text: string): string;
|
|
11
|
+
export function dedent(
|
|
12
|
+
stringsOrText: TemplateStringsArray | string,
|
|
13
|
+
...values: unknown[]
|
|
14
|
+
): string {
|
|
15
|
+
const raw =
|
|
16
|
+
typeof stringsOrText === "string"
|
|
17
|
+
? stringsOrText
|
|
18
|
+
: String.raw({ raw: stringsOrText }, ...values);
|
|
19
|
+
|
|
20
|
+
let lines = raw.split("\n");
|
|
21
|
+
|
|
22
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
23
|
+
lines = lines.slice(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
27
|
+
lines = lines.slice(0, lines.length - 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (lines.length === 0) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let minIndentLength = Number.POSITIVE_INFINITY;
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (line.trim() !== "") {
|
|
37
|
+
const indent = line.match(/^[ \t]*/)?.[0];
|
|
38
|
+
if (indent != null && indent.length < minIndentLength) {
|
|
39
|
+
minIndentLength = indent.length;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (minIndentLength === Number.POSITIVE_INFINITY) {
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines = lines.map((line) => {
|
|
49
|
+
if (line.trim() === "") {
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
return line.startsWith(" ".repeat(minIndentLength)) ||
|
|
53
|
+
line.startsWith("\t".repeat(minIndentLength))
|
|
54
|
+
? line.substring(minIndentLength)
|
|
55
|
+
: line;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
package/src/Util/index.ts
CHANGED