@davidvornholt/standards 0.21.0 → 0.22.0

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.
@@ -0,0 +1,53 @@
1
+ // Reads one plain (unencrypted) dev env layer from disk, distinguishing a
2
+ // missing optional file from an unreadable or unparsable one.
3
+
4
+ import { lstatSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { parseYaml } from './yaml-parse';
7
+
8
+ export type DevEnvPlainInput = { readonly raw: unknown } | null;
9
+
10
+ export type PlainLayerRead = {
11
+ readonly input: DevEnvPlainInput;
12
+ readonly present: boolean;
13
+ readonly problems: ReadonlyArray<string>;
14
+ };
15
+
16
+ const isMissingPathError = (error: unknown): boolean =>
17
+ (error as { readonly code?: unknown } | null)?.code === 'ENOENT';
18
+
19
+ export const readPlainLayer = (
20
+ consumer: string,
21
+ rel: string,
22
+ ): PlainLayerRead => {
23
+ const path = join(consumer, rel);
24
+ try {
25
+ lstatSync(path);
26
+ } catch (error) {
27
+ if (isMissingPathError(error)) {
28
+ return { input: null, present: false, problems: [] };
29
+ }
30
+ const detail = error instanceof Error ? error.message : String(error);
31
+ return {
32
+ input: null,
33
+ present: true,
34
+ problems: [`could not inspect ${rel}: ${detail}`],
35
+ };
36
+ }
37
+ let raw: string;
38
+ try {
39
+ raw = readFileSync(path, 'utf8');
40
+ } catch (error) {
41
+ const detail = error instanceof Error ? error.message : String(error);
42
+ return {
43
+ input: null,
44
+ present: true,
45
+ problems: [`could not read ${rel}: ${detail}`],
46
+ };
47
+ }
48
+ const parsed = parseYaml(raw, rel);
49
+ if (parsed.problem !== null) {
50
+ return { input: null, present: true, problems: [parsed.problem] };
51
+ }
52
+ return { input: { raw: parsed.value ?? {} }, present: true, problems: [] };
53
+ };
package/src/dev-env.ts CHANGED
@@ -1,5 +1,10 @@
1
- import { existsSync, lstatSync, readFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import {
4
+ type DevEnvRunOptions,
5
+ renderDevEnvPreservingUnsafeReferences,
6
+ } from './dev-env-brokered-preserve';
7
+ import { resolveBrokeredReferences } from './dev-env-brokered-resolve';
3
8
  import { composeDevEnv } from './dev-env-compose';
4
9
  import {
5
10
  type DevEnvRemoval,
@@ -8,64 +13,28 @@ import {
8
13
  } from './dev-env-destination';
9
14
  import { devEnvGitIgnoreProblem } from './dev-env-destination-gitignore';
10
15
  import { parseDevEnvDocument } from './dev-env-document';
11
- import { renderDotenv } from './dev-env-dotenv';
16
+ import type { DevEnvPlainInput } from './dev-env-plain-layer';
17
+ import { readPlainLayer } from './dev-env-plain-layer';
12
18
  import { planDevEnvRemovals } from './dev-env-reconciliation';
13
19
  import { DEV_SECRETS_FILE, readDevSecrets } from './dev-env-secrets';
14
20
  import { applyDevEnvChanges } from './dev-env-transaction';
15
- import { parseYaml } from './yaml-parse';
16
21
 
17
22
  export const DEV_CONFIG_FILE = 'config/dev.yaml';
18
23
  export const DEV_LOCAL_FILE = 'config/dev.local.yaml';
19
24
 
20
- export type DevEnvPlainInput = { readonly raw: unknown } | null;
21
-
22
25
  export type DevEnvInputs = {
23
26
  readonly config: DevEnvPlainInput;
24
27
  readonly secrets: unknown;
25
28
  readonly local: DevEnvPlainInput;
26
29
  };
27
30
 
28
- const isMissingPathError = (error: unknown): boolean =>
29
- (error as { readonly code?: unknown } | null)?.code === 'ENOENT';
30
-
31
- const readPlainLayer = (consumer: string, rel: string) => {
32
- const path = join(consumer, rel);
33
- try {
34
- lstatSync(path);
35
- } catch (error) {
36
- if (isMissingPathError(error)) {
37
- return { input: null, present: false, problems: [] };
38
- }
39
- const detail = error instanceof Error ? error.message : String(error);
40
- return {
41
- input: null,
42
- present: true,
43
- problems: [`could not inspect ${rel}: ${detail}`],
44
- };
45
- }
46
- let raw: string;
47
- try {
48
- raw = readFileSync(path, 'utf8');
49
- } catch (error) {
50
- const detail = error instanceof Error ? error.message : String(error);
51
- return {
52
- input: null,
53
- present: true,
54
- problems: [`could not read ${rel}: ${detail}`],
55
- };
56
- }
57
- const parsed = parseYaml(raw, rel);
58
- if (parsed.problem !== null) {
59
- return { input: null, present: true, problems: [parsed.problem] };
60
- }
61
- return { input: { raw: parsed.value ?? {} }, present: true, problems: [] };
62
- };
63
-
64
31
  const documentProblems = (
65
32
  source: string,
66
33
  input: DevEnvPlainInput,
67
34
  ): ReadonlyArray<string> =>
68
- input === null ? [] : parseDevEnvDocument(input.raw, source).problems;
35
+ input === null
36
+ ? []
37
+ : parseDevEnvDocument(input.raw, source, 'configuration').problems;
69
38
 
70
39
  export type DevEnvPlan = {
71
40
  readonly writes: ReadonlyArray<DevEnvWrite>;
@@ -76,35 +45,60 @@ export type DevEnvPlan = {
76
45
  export const planDevEnvChanges = (
77
46
  consumer: string,
78
47
  inputs: DevEnvInputs,
48
+ options: DevEnvRunOptions = { preservedBrokeredReferences: new Set() },
79
49
  ): DevEnvPlan => {
80
50
  const layer = (source: string, input: DevEnvPlainInput) =>
81
51
  input === null
82
52
  ? null
83
- : { source, document: parseDevEnvDocument(input.raw, source) };
53
+ : {
54
+ source,
55
+ document: parseDevEnvDocument(input.raw, source, 'configuration'),
56
+ };
84
57
  const composed = composeDevEnv(
85
58
  layer(DEV_CONFIG_FILE, inputs.config),
86
59
  {
87
60
  source: DEV_SECRETS_FILE,
88
- document: parseDevEnvDocument(inputs.secrets, DEV_SECRETS_FILE),
61
+ document: parseDevEnvDocument(
62
+ inputs.secrets,
63
+ DEV_SECRETS_FILE,
64
+ 'secrets',
65
+ ),
89
66
  },
90
67
  layer(DEV_LOCAL_FILE, inputs.local),
91
68
  );
92
- const problems: Array<string> = [...composed.problems];
69
+ const resolved = resolveBrokeredReferences(
70
+ consumer,
71
+ composed.targets,
72
+ composed.brokeredReferences,
73
+ options.preservedBrokeredReferences,
74
+ );
75
+ const problems: Array<string> = [...composed.problems, ...resolved.problems];
93
76
  const writes: Array<DevEnvWrite> = [];
94
- for (const target of composed.targets) {
77
+ for (const target of resolved.targets) {
95
78
  const workspaceDir = `${target.group}/${target.workspace}`;
96
79
  const rel = `${workspaceDir}/.env.local`;
97
80
  if (existsSync(join(consumer, workspaceDir, 'package.json'))) {
98
81
  const ignoreProblem = devEnvGitIgnoreProblem(consumer, rel);
99
82
  if (ignoreProblem === null) {
100
- writes.push({
83
+ const composedTarget = composed.targets.find(
84
+ (candidate) =>
85
+ candidate.group === target.group &&
86
+ candidate.workspace === target.workspace,
87
+ );
88
+ const rendered = renderDevEnvPreservingUnsafeReferences({
89
+ consumer,
101
90
  rel,
102
- content: renderDotenv(
103
- `${target.group}.${target.workspace}`,
104
- target.sources,
105
- target.env,
106
- ),
91
+ sourcePath: `${target.group}.${target.workspace}`,
92
+ sources: target.sources,
93
+ resolvedEnv: target.env,
94
+ composedEnv: composedTarget?.env ?? {},
95
+ preservedReferences: options.preservedBrokeredReferences,
107
96
  });
97
+ if (rendered.ok) {
98
+ writes.push({ rel, content: rendered.content });
99
+ } else {
100
+ problems.push(rendered.problem);
101
+ }
108
102
  } else {
109
103
  problems.push(ignoreProblem);
110
104
  }
@@ -122,7 +116,10 @@ export const planDevEnvChanges = (
122
116
  };
123
117
  };
124
118
 
125
- export const runDevEnv = async (consumer: string): Promise<boolean> => {
119
+ export const runDevEnv = async (
120
+ consumer: string,
121
+ options: DevEnvRunOptions = { preservedBrokeredReferences: new Set() },
122
+ ): Promise<boolean> => {
126
123
  const config = readPlainLayer(consumer, DEV_CONFIG_FILE);
127
124
  const secrets = readDevSecrets(consumer);
128
125
  const local = readPlainLayer(consumer, DEV_LOCAL_FILE);
@@ -136,7 +133,7 @@ export const runDevEnv = async (consumer: string): Promise<boolean> => {
136
133
  ...(localIgnoreProblem === null ? [] : [localIgnoreProblem]),
137
134
  ...documentProblems(DEV_CONFIG_FILE, config.input),
138
135
  ...(secrets.ok
139
- ? parseDevEnvDocument(secrets.value, DEV_SECRETS_FILE).problems
136
+ ? parseDevEnvDocument(secrets.value, DEV_SECRETS_FILE, 'secrets').problems
140
137
  : []),
141
138
  ...documentProblems(DEV_LOCAL_FILE, local.input),
142
139
  ];
@@ -145,11 +142,15 @@ export const runDevEnv = async (consumer: string): Promise<boolean> => {
145
142
  console.error(inputProblems.map((problem) => ` - ${problem}`).join('\n'));
146
143
  return false;
147
144
  }
148
- const plan = planDevEnvChanges(consumer, {
149
- config: config.input,
150
- secrets: secrets.value,
151
- local: local.input,
152
- });
145
+ const plan = planDevEnvChanges(
146
+ consumer,
147
+ {
148
+ config: config.input,
149
+ secrets: secrets.value,
150
+ local: local.input,
151
+ },
152
+ options,
153
+ );
153
154
  const problems = [
154
155
  ...plan.problems,
155
156
  ...(await devEnvDestinationProblems(consumer, [