@fabrika/provider-zerops 0.0.26 → 0.3.1
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 +18 -5
- package/src/api/client.ts +522 -0
- package/src/api/errors.ts +50 -0
- package/src/api/polling.ts +59 -0
- package/src/api/readers.ts +185 -0
- package/src/api/service-env-response.ts +34 -0
- package/src/api/types.ts +602 -0
- package/src/api.ts +29 -1378
- package/src/authoring.ts +7 -10
- package/src/cli-args.ts +0 -5
- package/src/cli.ts +3 -28
- package/src/collaborators.ts +2 -4
- package/src/compile.ts +75 -20
- package/src/control.ts +217 -447
- package/src/descriptor.ts +135 -0
- package/src/index.ts +24 -128
- package/src/installation/admin-options.ts +128 -0
- package/src/installation/admin.ts +123 -0
- package/src/installation/deploy-options.ts +155 -0
- package/src/installation/deploy.ts +668 -0
- package/src/installation/generated/platform-console-schema.ts +41 -0
- package/src/installation/generated/platform-proxy-manifest.ts +72 -0
- package/src/installation/hosts.ts +117 -0
- package/src/installation/index.ts +366 -0
- package/src/installation/init.ts +521 -0
- package/src/installation/install-collaborators.ts +28 -0
- package/src/installation/install-environment.ts +193 -0
- package/src/installation/install-options.ts +193 -0
- package/src/installation/install-project.ts +201 -0
- package/src/installation/install-provisioning.ts +185 -0
- package/src/installation/install.ts +286 -0
- package/src/installation/installation-source.ts +42 -0
- package/src/installation/log.ts +77 -0
- package/src/installation/machine-key.ts +65 -0
- package/src/installation/manifest.ts +95 -0
- package/src/installation/placement-options.ts +33 -0
- package/src/installation/platform-source.ts +106 -0
- package/src/installation/platform-upload.ts +34 -0
- package/src/installation/proxy-manifest.ts +113 -0
- package/src/installation/secrets.ts +79 -0
- package/src/installation/sidecar.ts +122 -0
- package/src/installation/source-options.ts +12 -0
- package/src/installation/templates/README.md +116 -0
- package/src/installation/templates/fabrika.ref +1 -0
- package/src/installation/templates/gitignore +3 -0
- package/src/installation/templates/platform.yml +144 -0
- package/src/installation/topology/artifacts.ts +141 -0
- package/src/installation/topology/index.ts +13 -0
- package/src/installation/topology/installation-schemas.ts +225 -0
- package/src/installation/topology/invariants.ts +86 -0
- package/src/installation/topology/setups.ts +442 -0
- package/src/installation/topology/topology.ts +501 -0
- package/src/installation/topology/validate.ts +26 -0
- package/src/installation/upgrade-options.ts +108 -0
- package/src/installation/upgrade-output.ts +31 -0
- package/src/installation/upgrade.ts +373 -0
- package/src/managed-env.ts +105 -0
- package/src/manifest.ts +208 -315
- package/src/namespace/claims.ts +77 -0
- package/src/namespace/codec.ts +179 -0
- package/src/namespace/constants.ts +30 -0
- package/src/namespace/errors.ts +60 -0
- package/src/namespace/manifest.ts +28 -0
- package/src/namespace/operator.ts +151 -0
- package/src/namespace/provisioning.ts +482 -0
- package/src/namespace/proxy-attempt.ts +220 -0
- package/src/namespace/proxy-source.ts +22 -0
- package/src/namespace/topology.ts +115 -0
- package/src/namespace/types.ts +114 -0
- package/src/namespace.ts +34 -1260
- package/src/plan.ts +21 -23
- package/src/provider.ts +399 -254
- package/src/service-references.ts +28 -0
- package/src/source/constants.ts +11 -0
- package/src/source/port.ts +49 -0
- package/src/source/types.ts +4 -0
- package/src/source/validation.ts +72 -0
- package/src/source.ts +2 -1220
- package/src/types.ts +8 -7
- package/src/version-attempt.ts +18 -0
- package/src/codec.ts +0 -108
- package/src/namespace-command.ts +0 -209
- package/src/source-connection.ts +0 -344
package/src/authoring.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ZEROPS_SHARED_POSTGRES_CONNECTION_STRING } from './namespace'
|
|
2
2
|
export { zeropsSharedServiceHostname, zeropsSharedServicePrefix } from './service-names'
|
|
3
3
|
import { APP_PROVIDER, isProviderAuthoredApp, type ProviderAuthoredApp } from '@fabrika/provider-contract'
|
|
4
|
+
import { z } from 'zod'
|
|
4
5
|
import type { ZeropsAppConfig, ZeropsSharedPostgresBinding } from './types'
|
|
5
6
|
|
|
6
7
|
/** Declare consumption of the namespace-owned `postgres` service without resolving its credential. */
|
|
@@ -20,14 +21,10 @@ export const defineApp = (config: ZeropsAppConfig): ZeropsAuthoredAppConfig => {
|
|
|
20
21
|
return { ...config, [APP_PROVIDER]: 'zerops' }
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const zeropsAppConfigSchema = z.object({
|
|
25
|
+
id: z.string().refine((id) => id.trim() !== ''),
|
|
26
|
+
target: z.object({ platform: z.literal('zerops'), services: z.function() }),
|
|
27
|
+
})
|
|
24
28
|
|
|
25
|
-
export const isZeropsAppConfig = (value: unknown): value is ZeropsAuthoredAppConfig =>
|
|
26
|
-
|
|
27
|
-
const target = property(value, 'target')
|
|
28
|
-
return isProviderAuthoredApp(value, 'zerops')
|
|
29
|
-
&& typeof id === 'string'
|
|
30
|
-
&& id.trim() !== ''
|
|
31
|
-
&& property(target, 'platform') === 'zerops'
|
|
32
|
-
&& typeof property(target, 'services') === 'function'
|
|
33
|
-
}
|
|
29
|
+
export const isZeropsAppConfig = (value: unknown): value is ZeropsAuthoredAppConfig =>
|
|
30
|
+
isProviderAuthoredApp(value, 'zerops') && zeropsAppConfigSchema.safeParse(value).success
|
package/src/cli-args.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface ZeropsCliArgs {
|
|
|
13
13
|
publicAccess: string | undefined
|
|
14
14
|
postgresType: string | undefined
|
|
15
15
|
postgresProfile: string | undefined
|
|
16
|
-
proxyBuildFromGit: string | undefined
|
|
17
16
|
controlUrl: string | undefined
|
|
18
17
|
help: boolean
|
|
19
18
|
}
|
|
@@ -33,7 +32,6 @@ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
|
|
|
33
32
|
let publicAccess: string | undefined
|
|
34
33
|
let postgresType: string | undefined
|
|
35
34
|
let postgresProfile: string | undefined
|
|
36
|
-
let proxyBuildFromGit: string | undefined
|
|
37
35
|
let controlUrl: string | undefined
|
|
38
36
|
let help = false
|
|
39
37
|
|
|
@@ -64,8 +62,6 @@ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
|
|
|
64
62
|
postgresType = arg.slice('--postgres-type='.length)
|
|
65
63
|
} else if (arg.startsWith('--postgres-profile=')) {
|
|
66
64
|
postgresProfile = arg.slice('--postgres-profile='.length)
|
|
67
|
-
} else if (arg.startsWith('--proxy-build-from-git=')) {
|
|
68
|
-
proxyBuildFromGit = arg.slice('--proxy-build-from-git='.length)
|
|
69
65
|
} else if (arg.startsWith('--control-url=')) {
|
|
70
66
|
controlUrl = arg.slice('--control-url='.length)
|
|
71
67
|
} else if (arg.startsWith('-')) {
|
|
@@ -94,7 +90,6 @@ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
|
|
|
94
90
|
publicAccess,
|
|
95
91
|
postgresType,
|
|
96
92
|
postgresProfile,
|
|
97
|
-
proxyBuildFromGit,
|
|
98
93
|
controlUrl,
|
|
99
94
|
help,
|
|
100
95
|
}
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { constants } from 'node:fs'
|
|
3
3
|
import { open } from 'node:fs/promises'
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
|
+
import { z } from 'zod'
|
|
5
6
|
import { isZeropsAppConfig } from './authoring'
|
|
6
7
|
import { parseZeropsCliArgs } from './cli-args'
|
|
7
8
|
import {
|
|
@@ -10,44 +11,26 @@ import {
|
|
|
10
11
|
manifestServiceHostnames,
|
|
11
12
|
ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES,
|
|
12
13
|
} from './manifest'
|
|
13
|
-
import { runZeropsNamespaceCommand } from './namespace-command'
|
|
14
14
|
import type { ZeropsAppConfig } from './types'
|
|
15
15
|
|
|
16
16
|
const USAGE = `Zerops app tools
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
19
|
fabrika app build --provider=zerops --env=<env> [--config=<path>] [--output=<path>]
|
|
20
|
-
fabrika namespace plan --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options]
|
|
21
|
-
fabrika namespace create --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options] [--control-url=<url>]
|
|
22
|
-
fabrika namespace adopt --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> --project-id=<id> [namespace options] [--control-url=<url>]
|
|
23
|
-
fabrika namespace reconcile --provider=zerops --id=<id> [--control-url=<url>]
|
|
24
20
|
|
|
25
21
|
Options:
|
|
26
22
|
--env=<env> Environment.
|
|
27
23
|
--config=<path> App config (default: ./fabrika.config.ts).
|
|
28
24
|
--output=<path> Artifact path (default: ./fabrika.manifest.json).
|
|
29
|
-
--id=<id> Namespace id.
|
|
30
|
-
--preset=<cheap|mid|full> Provider-owned namespace preset.
|
|
31
|
-
--exclusive-app=<id> App reserved by a full namespace.
|
|
32
|
-
--project-id=<id> Existing Zerops project id for adoption.
|
|
33
|
-
--project-name=<name> Zerops project name (default: namespace id).
|
|
34
|
-
--core-package=<tier> LIGHT or SERIOUS.
|
|
35
|
-
--public-access=<mode> custom-domain or zerops-subdomain.
|
|
36
|
-
--postgres-type=<type> Cheap-tier PostgreSQL type.
|
|
37
|
-
--postgres-profile=<profile> Cheap-tier PostgreSQL profile.
|
|
38
|
-
--proxy-build-from-git=<url> Public proxy source; or FABRIKA_ZEROPS_PROXY_BUILD_FROM_GIT.
|
|
39
|
-
--control-url=<url> Control origin; or FABRIKA_CONTROL_URL.
|
|
40
25
|
-h, --help Show this help.
|
|
41
|
-
|
|
42
|
-
Mutating namespace commands read the Bearer credential from FABRIKA_CONTROL_TOKEN.
|
|
43
26
|
`
|
|
44
27
|
|
|
45
|
-
const
|
|
28
|
+
const configModuleSchema = z.object({ default: z.unknown() })
|
|
46
29
|
|
|
47
30
|
const loadConfig = async (path: string): Promise<ZeropsAppConfig> => {
|
|
48
31
|
const absolute = resolve(process.cwd(), path)
|
|
49
32
|
const loaded: unknown = await import(absolute)
|
|
50
|
-
const config =
|
|
33
|
+
const config = configModuleSchema.safeParse(loaded).data?.default
|
|
51
34
|
if (!isZeropsAppConfig(config)) {
|
|
52
35
|
throw new Error(`Config at ${absolute} must default-export defineApp({ target: { platform: 'zerops', ... } })`)
|
|
53
36
|
}
|
|
@@ -92,14 +75,6 @@ export const runZeropsCli = async (argv: readonly string[]): Promise<void> => {
|
|
|
92
75
|
console.info(USAGE)
|
|
93
76
|
return
|
|
94
77
|
}
|
|
95
|
-
if (args.command === 'namespace') {
|
|
96
|
-
await runZeropsNamespaceCommand(args, {
|
|
97
|
-
source: process.env,
|
|
98
|
-
fetch: globalThis.fetch,
|
|
99
|
-
write: (value) => console.info(value.trimEnd()),
|
|
100
|
-
})
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
78
|
if (args.command !== 'build') {
|
|
104
79
|
throw new Error(`Unknown command: ${args.command}\n${USAGE}`)
|
|
105
80
|
}
|
package/src/collaborators.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { reconcileSchema } from '@fabrika/auth'
|
|
2
2
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
3
3
|
import { createZeropsApi, type ZeropsApi } from './api'
|
|
4
|
-
import type {
|
|
4
|
+
import type { ZeropsSourcePort } from './source/port'
|
|
5
5
|
import type { ZeropsRuntimeTarget } from './types'
|
|
6
6
|
|
|
7
7
|
export type Sleeper = (ms: number, signal: AbortSignal) => Promise<void>
|
|
@@ -26,9 +26,7 @@ export const defaultSleep: Sleeper = (ms, signal) =>
|
|
|
26
26
|
export interface ZeropsCollaborators {
|
|
27
27
|
api: ZeropsApi
|
|
28
28
|
/** Injected by the Zerops control composition; dry runs do not require it. */
|
|
29
|
-
source?:
|
|
30
|
-
/** Independently bounds best-effort source cancellation without delaying Zerops cleanup. */
|
|
31
|
-
sourceCancelSleep?: Sleeper
|
|
29
|
+
source?: ZeropsSourcePort
|
|
32
30
|
reconcileSchema: SchemaReconciler
|
|
33
31
|
sleep: Sleeper
|
|
34
32
|
}
|
package/src/compile.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// credentials leak to app B. "Never write to project-level env" alone does not prevent that; the isolation
|
|
19
19
|
// setting is the other half.
|
|
20
20
|
|
|
21
|
+
import type { JsonValue } from '@fabrika/provider-contract'
|
|
21
22
|
import type { ZeropsImportProject, ZeropsImportService } from './schema.generated'
|
|
22
23
|
import type { ZeropsProjectSpec, ZeropsResourceContext, ZeropsServiceSpec, ZeropsSourceTarget } from './types'
|
|
23
24
|
|
|
@@ -37,6 +38,16 @@ export interface ZeropsImportDocument {
|
|
|
37
38
|
services: ZeropsImportService[]
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The same document once it has been through JSON — a manifest's stored `importDocument`, or one whose
|
|
43
|
+
* values were substituted. Structurally identical, leaves typed as JSON, so `assertZeropsInvariants`
|
|
44
|
+
* checks it too rather than trusting whoever produced it.
|
|
45
|
+
*/
|
|
46
|
+
export interface ZeropsJsonImportDocument {
|
|
47
|
+
readonly project?: { readonly [key: string]: JsonValue }
|
|
48
|
+
readonly services: ReadonlyArray<{ readonly [key: string]: JsonValue }>
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
/** What the compiler needs beyond the app's declaration. */
|
|
41
52
|
export interface CompileInput {
|
|
42
53
|
target: ZeropsSourceTarget
|
|
@@ -185,14 +196,14 @@ const compileProject = (spec: ZeropsProjectSpec): ZeropsImportProject => {
|
|
|
185
196
|
*
|
|
186
197
|
* Deliberately checks the emitted document rather than the input: it is the document that reaches Zerops.
|
|
187
198
|
*/
|
|
188
|
-
export const assertZeropsInvariants = (document: ZeropsImportDocument): void => {
|
|
199
|
+
export const assertZeropsInvariants = (document: ZeropsImportDocument | ZeropsJsonImportDocument): void => {
|
|
189
200
|
const project = document.project
|
|
190
201
|
if (project !== undefined) {
|
|
191
|
-
if (project
|
|
202
|
+
if (project['envVariables'] !== undefined) {
|
|
192
203
|
throw new Error('zerops: refusing to write PROJECT-level env variables — they reach every service in the project (ADR-0004)')
|
|
193
204
|
}
|
|
194
|
-
if (project
|
|
195
|
-
throw new Error(`zerops: project envIsolation must be \`${ENV_ISOLATION}\`, got \`${String(project
|
|
205
|
+
if (project['envIsolation'] !== ENV_ISOLATION) {
|
|
206
|
+
throw new Error(`zerops: project envIsolation must be \`${ENV_ISOLATION}\`, got \`${String(project['envIsolation'])}\` (ADR-0004)`)
|
|
196
207
|
}
|
|
197
208
|
}
|
|
198
209
|
if (document.services.length === 0) {
|
|
@@ -200,23 +211,24 @@ export const assertZeropsInvariants = (document: ZeropsImportDocument): void =>
|
|
|
200
211
|
}
|
|
201
212
|
const seen = new Set<string>()
|
|
202
213
|
for (const service of document.services) {
|
|
203
|
-
|
|
214
|
+
const hostname = String(service['hostname'])
|
|
215
|
+
if (service['envIsolation'] !== ENV_ISOLATION) {
|
|
204
216
|
throw new Error(
|
|
205
|
-
`zerops: service \`${
|
|
217
|
+
`zerops: service \`${hostname}\` envIsolation must be \`${ENV_ISOLATION}\`, got \`${String(service['envIsolation'])}\` (ADR-0004)`,
|
|
206
218
|
)
|
|
207
219
|
}
|
|
208
220
|
// Every service class, managed included. A document missing it anywhere cannot be re-applied at
|
|
209
221
|
// all: the platform answers `400 serviceStackNameUnavailable` on the first hostname that exists
|
|
210
222
|
// and rejects the whole import, so one omission breaks every later apply, not just that service.
|
|
211
|
-
if (service
|
|
223
|
+
if (service['override'] !== OVERRIDE) {
|
|
212
224
|
throw new Error(
|
|
213
|
-
`zerops: service \`${
|
|
225
|
+
`zerops: service \`${hostname}\` must carry \`override: true\`; without it a re-apply is rejected as a name collision`,
|
|
214
226
|
)
|
|
215
227
|
}
|
|
216
|
-
if (service
|
|
228
|
+
if (service['envSecrets'] !== undefined || service['dotEnvSecrets'] !== undefined) {
|
|
217
229
|
// Never assert on the VALUE — only that the field is present.
|
|
218
230
|
throw new Error(
|
|
219
|
-
`zerops: service \`${
|
|
231
|
+
`zerops: service \`${hostname}\` carries secrets in the import document; secrets are written through the env API (ADR-0004)`,
|
|
220
232
|
)
|
|
221
233
|
}
|
|
222
234
|
// `zeropsSetup` is part of the import's pipeline configuration, and the platform rejects the WHOLE
|
|
@@ -230,15 +242,15 @@ export const assertZeropsInvariants = (document: ZeropsImportDocument): void =>
|
|
|
230
242
|
// Nothing is lost by dropping the setup name here: it is only redundant restatement of Zerops' own
|
|
231
243
|
// "setup name defaults to the hostname" rule, and a deploy that needs to say otherwise passes
|
|
232
244
|
// `zeropsSetup` at TRIGGER time, where no repository is required.
|
|
233
|
-
if (service
|
|
245
|
+
if (service['zeropsSetup'] !== undefined && service['buildFromGit'] === undefined) {
|
|
234
246
|
throw new Error(
|
|
235
|
-
`zerops: service \`${
|
|
247
|
+
`zerops: service \`${hostname}\` names \`zeropsSetup\` without \`buildFromGit\`; the platform rejects the import (pass the setup when triggering the pipeline instead)`,
|
|
236
248
|
)
|
|
237
249
|
}
|
|
238
|
-
if (seen.has(
|
|
239
|
-
throw new Error(`zerops: duplicate service hostname \`${
|
|
250
|
+
if (seen.has(hostname)) {
|
|
251
|
+
throw new Error(`zerops: duplicate service hostname \`${hostname}\` in the import document`)
|
|
240
252
|
}
|
|
241
|
-
seen.add(
|
|
253
|
+
seen.add(hostname)
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
|
|
@@ -257,13 +269,57 @@ export const compileImport = (input: CompileInput): ZeropsImportDocument => {
|
|
|
257
269
|
/** Keys/scalars that are safe unquoted in YAML. Everything else gets a double-quoted, JSON-escaped form. */
|
|
258
270
|
const PLAIN = /^[A-Za-z_][A-Za-z0-9_./@+-]*$/
|
|
259
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Code points a YAML 1.1 reader (go-yaml, which parses this on Zerops) ends a line on or refuses outright,
|
|
274
|
+
* while Bun's 1.2 reader takes them for ordinary text — so no test using Bun can see the difference. `\n`
|
|
275
|
+
* and `\r` are excluded: the block form owns one and `blockScalarSafe` refuses the other.
|
|
276
|
+
*/
|
|
277
|
+
const isYamlHostileCodePoint = (code: number): boolean =>
|
|
278
|
+
(code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
|
|
279
|
+
|| (code >= 0x7f && code <= 0x9f)
|
|
280
|
+
|| code === 0x2028
|
|
281
|
+
|| code === 0x2029
|
|
282
|
+
|| code === 0xfeff
|
|
283
|
+
|| code === 0xfffe
|
|
284
|
+
|| code === 0xffff
|
|
285
|
+
|| (code >= 0xd800 && code <= 0xdfff)
|
|
286
|
+
|
|
287
|
+
/** `JSON.stringify` leaves DEL, the C1 block, U+2028/9 and U+FEFF raw; YAML's own `\uXXXX` carries them. */
|
|
288
|
+
const escapeYamlHostile = (quoted: string): string =>
|
|
289
|
+
[...quoted].map((character) => {
|
|
290
|
+
const code = character.codePointAt(0) ?? 0
|
|
291
|
+
return isYamlHostileCodePoint(code) ? `\\u${code.toString(16).padStart(4, '0')}` : character
|
|
292
|
+
}).join('')
|
|
293
|
+
|
|
260
294
|
const scalar = (value: string): string => {
|
|
261
295
|
// Reserved plain scalars that YAML would read as booleans/null, plus anything with structure.
|
|
262
|
-
const reserved = new Set(['true', 'false', 'null', 'yes', 'no', 'on', 'off', '~'])
|
|
296
|
+
const reserved = new Set(['true', 'false', 'null', 'yes', 'no', 'y', 'n', 'on', 'off', '~'])
|
|
263
297
|
if (value !== '' && PLAIN.test(value) && !reserved.has(value.toLowerCase())) {
|
|
264
298
|
return value
|
|
265
299
|
}
|
|
266
|
-
return JSON.stringify(value)
|
|
300
|
+
return escapeYamlHostile(JSON.stringify(value))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A `|-` block reproduces the string only when the text owns its own lines. An indented or blank first
|
|
305
|
+
* line moves (or breaks) the block's own indentation, `\r` and a blank line do not survive it, and `-`
|
|
306
|
+
* eats a trailing newline. A code point a YAML 1.1 reader breaks a line on cannot go in at all — the block
|
|
307
|
+
* is the one form with no escapes. Everything else goes out double-quoted, which is exact YAML.
|
|
308
|
+
*/
|
|
309
|
+
const blockScalarSafe = (value: string): boolean => {
|
|
310
|
+
if (!value.includes('\n') || value.includes('\r')) {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
if ([...value].some((character) => isYamlHostileCodePoint(character.codePointAt(0) ?? 0))) {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
const lines = value.split('\n')
|
|
317
|
+
const first = lines[0] ?? ''
|
|
318
|
+
if (first === '' || first.startsWith(' ') || first.startsWith('\t')) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
// A blank line loses its whitespace, and an empty last line is the trailing newline `-` strips.
|
|
322
|
+
return !lines.some((line, index) => line.trim() === '' && (line !== '' || index === lines.length - 1))
|
|
267
323
|
}
|
|
268
324
|
|
|
269
325
|
const isArray = (value: unknown): value is unknown[] => Array.isArray(value)
|
|
@@ -316,9 +372,8 @@ const emit = (value: unknown, indent: number): string[] => {
|
|
|
316
372
|
return lines
|
|
317
373
|
}
|
|
318
374
|
if (typeof value === 'string') {
|
|
319
|
-
if (value
|
|
320
|
-
|
|
321
|
-
return [`${pad}|-`, ...body]
|
|
375
|
+
if (blockScalarSafe(value)) {
|
|
376
|
+
return [`${pad}|-`, ...value.split('\n').map((line) => `${pad} ${line}`)]
|
|
322
377
|
}
|
|
323
378
|
return [`${pad}${scalar(value)}`]
|
|
324
379
|
}
|