@smoothbricks/cli 0.6.0 → 0.8.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.
- package/dist/monorepo/managed-files.d.ts +45 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +87 -2
- package/dist/monorepo/packed-package.js +8 -6
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +13 -0
- package/dist/monorepo/wrangler.d.ts +5 -0
- package/dist/monorepo/wrangler.d.ts.map +1 -0
- package/dist/monorepo/wrangler.js +122 -0
- package/package.json +1 -1
- package/src/monorepo/managed-files.test.ts +128 -0
- package/src/monorepo/managed-files.ts +100 -2
- package/src/monorepo/packed-package.ts +8 -6
- package/src/monorepo/packs/index.ts +13 -0
- package/src/monorepo/wrangler.test.ts +216 -0
- package/src/monorepo/wrangler.ts +135 -0
- package/src/pr/index.test.ts +2 -2
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repos may append their own content to a managed file below this marker —
|
|
3
|
+
* e.g. extra merge drivers in .gitattributes. Everything from the marker
|
|
4
|
+
* line onward is preserved verbatim across updates and ignored by the
|
|
5
|
+
* drift check; the managed section above it stays byte-exact.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LOCAL_SECTION_MARKER = "# smoo-local: everything below this line is repo-owned and preserved";
|
|
8
|
+
/** Split a managed target's content into the managed part and the repo-owned tail. */
|
|
9
|
+
declare function splitLocalSection(current: string): {
|
|
10
|
+
managed: string;
|
|
11
|
+
localTail: string;
|
|
12
|
+
};
|
|
13
|
+
/** Test seam for the pure splitter. */
|
|
14
|
+
export declare const splitLocalSectionForTest: typeof splitLocalSection;
|
|
15
|
+
/**
|
|
16
|
+
* A repo-owned block INSIDE the managed section — e.g. one extra pattern
|
|
17
|
+
* spliced into a formatter's list, where a trailing marker (LOCAL_SECTION_MARKER)
|
|
18
|
+
* can't express it because it isn't at the end of the file. Wrap it in
|
|
19
|
+
* `# smoo-local-begin` / `# smoo-local-end`; the block is anchored to the line
|
|
20
|
+
* immediately before `# smoo-local-begin`. On update, the block is re-spliced
|
|
21
|
+
* right after that same anchor line in the freshly rendered template — if the
|
|
22
|
+
* anchor no longer appears there (the template reworked that section), the
|
|
23
|
+
* update refuses rather than silently dropping the repo's customization.
|
|
24
|
+
*/
|
|
25
|
+
export declare const INLINE_LOCAL_BEGIN = "# smoo-local-begin";
|
|
26
|
+
export declare const INLINE_LOCAL_END = "# smoo-local-end";
|
|
27
|
+
interface InlineLocalBlock {
|
|
28
|
+
anchor: string;
|
|
29
|
+
lines: string;
|
|
30
|
+
}
|
|
31
|
+
/** Pull inline local blocks out of a managed section, returning the section
|
|
32
|
+
* with each block (and its markers) removed, plus the extracted blocks in
|
|
33
|
+
* the order they appeared. */
|
|
34
|
+
declare function extractInlineLocalBlocks(managed: string): {
|
|
35
|
+
withoutInline: string;
|
|
36
|
+
blocks: InlineLocalBlock[];
|
|
37
|
+
};
|
|
38
|
+
/** Test seam for the pure extractor. */
|
|
39
|
+
export declare const extractInlineLocalBlocksForTest: typeof extractInlineLocalBlocks;
|
|
40
|
+
/** Re-splice extracted inline blocks into freshly rendered managed content,
|
|
41
|
+
* each immediately after its anchor line. A no-op when there are no blocks. */
|
|
42
|
+
declare function reinsertInlineLocalBlocks(content: string, blocks: InlineLocalBlock[]): string;
|
|
43
|
+
/** Test seam for the pure re-splicer. */
|
|
44
|
+
export declare const reinsertInlineLocalBlocksForTest: typeof reinsertInlineLocalBlocks;
|
|
1
45
|
export interface FileResult {
|
|
2
46
|
target: string;
|
|
3
47
|
action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
|
|
@@ -5,4 +49,5 @@ export interface FileResult {
|
|
|
5
49
|
export declare function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[];
|
|
6
50
|
export declare function printResults(results: FileResult[]): void;
|
|
7
51
|
export declare function validateManagedFiles(root: string): number;
|
|
52
|
+
export {};
|
|
8
53
|
//# sourceMappingURL=managed-files.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,yEAAyE,CAAC;AAU3G,sFAAsF;AACtF,iBAAS,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAIlF;AAED,uCAAuC;AACvC,eAAO,MAAM,wBAAwB,0BAAoB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;8BAE8B;AAC9B,iBAAS,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CA6BxG;AAED,wCAAwC;AACxC,eAAO,MAAM,+BAA+B,iCAA2B,CAAC;AAExE;+EAC+E;AAC/E,iBAAS,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,CActF;AAED,yCAAyC;AACzC,eAAO,MAAM,gCAAgC,kCAA4B,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;CACxG;AA8GD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,EAAE,CAG/F;AAgLD,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
|
|
@@ -5,6 +5,87 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
6
6
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
7
7
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
8
|
+
/**
|
|
9
|
+
* Repos may append their own content to a managed file below this marker —
|
|
10
|
+
* e.g. extra merge drivers in .gitattributes. Everything from the marker
|
|
11
|
+
* line onward is preserved verbatim across updates and ignored by the
|
|
12
|
+
* drift check; the managed section above it stays byte-exact.
|
|
13
|
+
*/
|
|
14
|
+
export const LOCAL_SECTION_MARKER = '# smoo-local: everything below this line is repo-owned and preserved';
|
|
15
|
+
/** Split a managed target's content into the managed part and the repo-owned tail. */
|
|
16
|
+
function splitLocalSection(current) {
|
|
17
|
+
const index = current.indexOf(LOCAL_SECTION_MARKER);
|
|
18
|
+
if (index === -1)
|
|
19
|
+
return { managed: current, localTail: '' };
|
|
20
|
+
return { managed: current.slice(0, index), localTail: current.slice(index) };
|
|
21
|
+
}
|
|
22
|
+
/** Test seam for the pure splitter. */
|
|
23
|
+
export const splitLocalSectionForTest = splitLocalSection;
|
|
24
|
+
/**
|
|
25
|
+
* A repo-owned block INSIDE the managed section — e.g. one extra pattern
|
|
26
|
+
* spliced into a formatter's list, where a trailing marker (LOCAL_SECTION_MARKER)
|
|
27
|
+
* can't express it because it isn't at the end of the file. Wrap it in
|
|
28
|
+
* `# smoo-local-begin` / `# smoo-local-end`; the block is anchored to the line
|
|
29
|
+
* immediately before `# smoo-local-begin`. On update, the block is re-spliced
|
|
30
|
+
* right after that same anchor line in the freshly rendered template — if the
|
|
31
|
+
* anchor no longer appears there (the template reworked that section), the
|
|
32
|
+
* update refuses rather than silently dropping the repo's customization.
|
|
33
|
+
*/
|
|
34
|
+
export const INLINE_LOCAL_BEGIN = '# smoo-local-begin';
|
|
35
|
+
export const INLINE_LOCAL_END = '# smoo-local-end';
|
|
36
|
+
/** Pull inline local blocks out of a managed section, returning the section
|
|
37
|
+
* with each block (and its markers) removed, plus the extracted blocks in
|
|
38
|
+
* the order they appeared. */
|
|
39
|
+
function extractInlineLocalBlocks(managed) {
|
|
40
|
+
const lines = managed.split('\n');
|
|
41
|
+
const kept = [];
|
|
42
|
+
const blocks = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < lines.length) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
if (line !== undefined && line.trim() === INLINE_LOCAL_BEGIN) {
|
|
47
|
+
const anchor = kept.at(-1);
|
|
48
|
+
if (anchor === undefined) {
|
|
49
|
+
throw new Error(`${INLINE_LOCAL_BEGIN} on line ${i + 1} has no preceding anchor line`);
|
|
50
|
+
}
|
|
51
|
+
const blockLines = [];
|
|
52
|
+
i += 1;
|
|
53
|
+
while (i < lines.length && lines[i]?.trim() !== INLINE_LOCAL_END) {
|
|
54
|
+
blockLines.push(lines[i]);
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
if (i >= lines.length) {
|
|
58
|
+
throw new Error(`${INLINE_LOCAL_BEGIN} anchored on "${anchor}" has no matching ${INLINE_LOCAL_END}`);
|
|
59
|
+
}
|
|
60
|
+
blocks.push({ anchor, lines: blockLines.join('\n') });
|
|
61
|
+
i += 1; // skip the END marker line itself
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
kept.push(line);
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
return { withoutInline: kept.join('\n'), blocks };
|
|
68
|
+
}
|
|
69
|
+
/** Test seam for the pure extractor. */
|
|
70
|
+
export const extractInlineLocalBlocksForTest = extractInlineLocalBlocks;
|
|
71
|
+
/** Re-splice extracted inline blocks into freshly rendered managed content,
|
|
72
|
+
* each immediately after its anchor line. A no-op when there are no blocks. */
|
|
73
|
+
function reinsertInlineLocalBlocks(content, blocks) {
|
|
74
|
+
if (blocks.length === 0)
|
|
75
|
+
return content;
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
for (const block of blocks) {
|
|
78
|
+
const index = lines.indexOf(block.anchor);
|
|
79
|
+
if (index === -1) {
|
|
80
|
+
throw new Error(`${INLINE_LOCAL_BEGIN} block anchored on "${block.anchor}" no longer matches any line in the updated ` +
|
|
81
|
+
'template — reconcile the repo-owned block manually');
|
|
82
|
+
}
|
|
83
|
+
lines.splice(index + 1, 0, INLINE_LOCAL_BEGIN, ...block.lines.split('\n'), INLINE_LOCAL_END);
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
/** Test seam for the pure re-splicer. */
|
|
88
|
+
export const reinsertInlineLocalBlocksForTest = reinsertInlineLocalBlocks;
|
|
8
89
|
const managedFiles = [
|
|
9
90
|
{
|
|
10
91
|
kind: 'raw',
|
|
@@ -114,13 +195,17 @@ function applyManagedFile(root, file, mode, context) {
|
|
|
114
195
|
throw new Error(`${file.target} exists but is not a regular file or symlink`);
|
|
115
196
|
}
|
|
116
197
|
const current = readFileSync(target, 'utf8');
|
|
117
|
-
|
|
198
|
+
const { managed, localTail } = splitLocalSection(current);
|
|
199
|
+
const { withoutInline, blocks } = extractInlineLocalBlocks(managed);
|
|
200
|
+
if (withoutInline === content || (localTail !== '' && withoutInline === `${content}\n`)) {
|
|
118
201
|
return { target: file.target, action: 'unchanged' };
|
|
119
202
|
}
|
|
120
203
|
if (mode === 'check' || mode === 'diff') {
|
|
121
204
|
return { target: file.target, action: 'drifted' };
|
|
122
205
|
}
|
|
123
|
-
|
|
206
|
+
const rendered = reinsertInlineLocalBlocks(content, blocks);
|
|
207
|
+
const next = localTail === '' ? rendered : `${rendered}\n${localTail}`;
|
|
208
|
+
writeManagedFile(target, next, file.executable === true);
|
|
124
209
|
return { target: file.target, action: 'updated' };
|
|
125
210
|
}
|
|
126
211
|
if (mode === 'check' || mode === 'diff') {
|
|
@@ -69,7 +69,7 @@ async function validatePackedManifest(root, pkg, packed) {
|
|
|
69
69
|
async function validateAttw(root, pkg, packed) {
|
|
70
70
|
const attw = await loadAttwCore();
|
|
71
71
|
const analysis = await attw.checkPackage(await createAttwPackageFromTarball(attw, root, packed.path), {
|
|
72
|
-
excludeEntrypoints:
|
|
72
|
+
excludeEntrypoints: nonJsExportEntrypoints(pkg.json.exports),
|
|
73
73
|
});
|
|
74
74
|
if (!isRecord(analysis) || analysis.types === false) {
|
|
75
75
|
return 0;
|
|
@@ -205,19 +205,21 @@ function formatResolutionKind(kind) {
|
|
|
205
205
|
}
|
|
206
206
|
return kind;
|
|
207
207
|
}
|
|
208
|
-
function
|
|
208
|
+
function nonJsExportEntrypoints(exports) {
|
|
209
209
|
if (!isRecord(exports)) {
|
|
210
210
|
return [];
|
|
211
211
|
}
|
|
212
212
|
return Object.entries(exports)
|
|
213
|
-
.filter(([key, value]) => key.startsWith('.') &&
|
|
213
|
+
.filter(([key, value]) => key.startsWith('.') && exportPointsToNonJs(value))
|
|
214
214
|
.map(([key]) => key);
|
|
215
215
|
}
|
|
216
|
-
function
|
|
216
|
+
function exportPointsToNonJs(value) {
|
|
217
217
|
if (typeof value === 'string') {
|
|
218
|
-
|
|
218
|
+
// attw resolves every entrypoint as a module; assets can never have types.
|
|
219
|
+
// Existence of the target files is still validated by publint.
|
|
220
|
+
return value.endsWith('.wasm') || value.endsWith('.css');
|
|
219
221
|
}
|
|
220
|
-
return isRecord(value) && Object.values(value).some(
|
|
222
|
+
return isRecord(value) && Object.values(value).some(exportPointsToNonJs);
|
|
221
223
|
}
|
|
222
224
|
async function packPackage(root, pkg) {
|
|
223
225
|
const packageDir = join(root, pkg.path);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/monorepo/packs/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/monorepo/packs/index.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,WAAW,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,YAAY,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1D,gBAAgB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAClE,iBAAiB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CACpE;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC9F;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AA4GD,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,mBAAwB,EACjC,KAAK,GAAE,oBAAyB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CA0B7B"}
|
|
@@ -10,6 +10,7 @@ import { applyFixableMonorepoDefaults, applyNxReleaseDefaults, applyPublicPackag
|
|
|
10
10
|
import { validatePackedPublicPackageManifest, validatePackedPublicPackagePublint, validatePackedPublicPackageTypes, } from '../packed-package.js';
|
|
11
11
|
import { syncRootRuntimeVersions } from '../runtime.js';
|
|
12
12
|
import { applyToolConfigDefaults, validateToolConfig } from '../tool-validation.js';
|
|
13
|
+
import { applyWranglerDefaults, validateWrangler } from '../wrangler.js';
|
|
13
14
|
const packs = [
|
|
14
15
|
{
|
|
15
16
|
name: 'core',
|
|
@@ -96,6 +97,18 @@ const packs = [
|
|
|
96
97
|
return validatePackedPublicPackageTypes(ctx.root);
|
|
97
98
|
},
|
|
98
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: 'wrangler',
|
|
102
|
+
init(ctx) {
|
|
103
|
+
applyWranglerDefaults(ctx.root);
|
|
104
|
+
},
|
|
105
|
+
fixPreBuild(ctx) {
|
|
106
|
+
applyWranglerDefaults(ctx.root);
|
|
107
|
+
},
|
|
108
|
+
validatePreBuild(ctx) {
|
|
109
|
+
return validateWrangler(ctx.root);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
99
112
|
];
|
|
100
113
|
export async function runInitPacks(ctx) {
|
|
101
114
|
for (const pack of packs) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** First `[env.<name>]` header in a wrangler.toml, or `null` when the config declares no envs (top-level bindings only). */
|
|
2
|
+
export declare function firstWranglerEnv(tomlText: string): string | null;
|
|
3
|
+
export declare function applyWranglerDefaults(root: string): void;
|
|
4
|
+
export declare function validateWrangler(root: string): number;
|
|
5
|
+
//# sourceMappingURL=wrangler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrangler.d.ts","sourceRoot":"","sources":["../../src/monorepo/wrangler.ts"],"names":[],"mappings":"AAaA,4HAA4H;AAC5H,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAkCD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAiDxD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgCrD"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
|
|
4
|
+
import { getWorkspacePackageManifests } from '../lib/workspace.js';
|
|
5
|
+
/** First `[env.<name>]` header in a wrangler.toml, or `null` when the config declares no envs (top-level bindings only). */
|
|
6
|
+
export function firstWranglerEnv(tomlText) {
|
|
7
|
+
const match = tomlText.match(/^\s*\[env\.([A-Za-z0-9_-]+)/m);
|
|
8
|
+
return match ? match[1] : null;
|
|
9
|
+
}
|
|
10
|
+
function wranglerProjects(root) {
|
|
11
|
+
const projects = [];
|
|
12
|
+
for (const pkg of getWorkspacePackageManifests(root)) {
|
|
13
|
+
const tomlPath = join(pkg.path, 'wrangler.toml');
|
|
14
|
+
if (!existsSync(tomlPath)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
projects.push({
|
|
18
|
+
label: relative(root, pkg.path) || '.',
|
|
19
|
+
dir: pkg.path,
|
|
20
|
+
packageJsonPath: pkg.packageJsonPath,
|
|
21
|
+
json: pkg.json,
|
|
22
|
+
tomlPath,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return projects;
|
|
26
|
+
}
|
|
27
|
+
/** Ensure the root .gitignore ignores the local secret file + generated types (once). */
|
|
28
|
+
function ensureGitignore(root) {
|
|
29
|
+
const path = join(root, '.gitignore');
|
|
30
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
31
|
+
const lines = text.split('\n').map((line) => line.trim());
|
|
32
|
+
const missing = ['.dev.vars', 'worker-configuration.d.ts'].filter((entry) => !lines.includes(entry));
|
|
33
|
+
if (missing.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const block = `${text.replace(/\s*$/, '')}\n\n# wrangler: local secret values + generated types (names live in .dev.vars.example)\n${missing.join('\n')}\n`;
|
|
37
|
+
writeFileSync(path, block);
|
|
38
|
+
console.log(`updated .gitignore (${missing.join(', ')})`);
|
|
39
|
+
}
|
|
40
|
+
export function applyWranglerDefaults(root) {
|
|
41
|
+
const projects = wranglerProjects(root);
|
|
42
|
+
if (projects.length > 0) {
|
|
43
|
+
ensureGitignore(root);
|
|
44
|
+
}
|
|
45
|
+
for (const project of projects) {
|
|
46
|
+
const env = firstWranglerEnv(readFileSync(project.tomlPath, 'utf8'));
|
|
47
|
+
const nx = getOrCreateRecord(project.json, 'nx');
|
|
48
|
+
const targets = getOrCreateRecord(nx, 'targets');
|
|
49
|
+
const desired = {
|
|
50
|
+
executor: 'nx:run-commands',
|
|
51
|
+
cache: true,
|
|
52
|
+
inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
|
|
53
|
+
outputs: ['{projectRoot}/worker-configuration.d.ts'],
|
|
54
|
+
options: {
|
|
55
|
+
command: `wrangler types${env ? ` --env ${env}` : ''} --env-file .dev.vars.example --include-runtime false`,
|
|
56
|
+
cwd: '{projectRoot}',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
let changed = false;
|
|
60
|
+
const existing = recordProperty(targets, 'wrangler-types');
|
|
61
|
+
if (!existing || JSON.stringify(existing) !== JSON.stringify(desired)) {
|
|
62
|
+
targets['wrangler-types'] = desired;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
const typecheck = recordProperty(targets, 'typecheck');
|
|
66
|
+
if (typecheck) {
|
|
67
|
+
const dependsOn = Array.isArray(typecheck.dependsOn) ? typecheck.dependsOn : [];
|
|
68
|
+
if (!dependsOn.includes('wrangler-types')) {
|
|
69
|
+
dependsOn.push('wrangler-types');
|
|
70
|
+
typecheck.dependsOn = dependsOn;
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (changed) {
|
|
75
|
+
writeJsonObject(project.packageJsonPath, project.json);
|
|
76
|
+
console.log(`updated ${project.label}/package.json wrangler-types target`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log(`unchanged ${project.label}/package.json wrangler-types target`);
|
|
80
|
+
}
|
|
81
|
+
// Empty .dev.vars.example = "this worker declares no secrets". Bootstrap it
|
|
82
|
+
// when missing; humans add SECRET_NAME= lines if the worker reads any (the
|
|
83
|
+
// wrangler-types + tsc gate fails on an undeclared secret until they do).
|
|
84
|
+
const examplePath = join(project.dir, '.dev.vars.example');
|
|
85
|
+
if (!existsSync(examplePath)) {
|
|
86
|
+
writeFileSync(examplePath, '');
|
|
87
|
+
console.log(`created ${project.label}/.dev.vars.example (no secrets)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function validateWrangler(root) {
|
|
92
|
+
const gitignorePath = join(root, '.gitignore');
|
|
93
|
+
const gitignoreLines = existsSync(gitignorePath)
|
|
94
|
+
? readFileSync(gitignorePath, 'utf8')
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
: [];
|
|
98
|
+
const gitignoreCoversSecrets = gitignoreLines.includes('.dev.vars') && gitignoreLines.includes('worker-configuration.d.ts');
|
|
99
|
+
let problems = 0;
|
|
100
|
+
for (const project of wranglerProjects(root)) {
|
|
101
|
+
let projectProblems = 0;
|
|
102
|
+
if (!existsSync(join(project.dir, '.dev.vars.example'))) {
|
|
103
|
+
console.log(`⨯ ${project.label}: missing .dev.vars.example`);
|
|
104
|
+
projectProblems++;
|
|
105
|
+
}
|
|
106
|
+
const nx = recordProperty(project.json, 'nx');
|
|
107
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
108
|
+
if (!targets || !recordProperty(targets, 'wrangler-types')) {
|
|
109
|
+
console.log(`⨯ ${project.label}: missing wrangler-types nx target`);
|
|
110
|
+
projectProblems++;
|
|
111
|
+
}
|
|
112
|
+
if (!gitignoreCoversSecrets) {
|
|
113
|
+
console.log(`⨯ ${project.label}: root .gitignore must ignore .dev.vars and worker-configuration.d.ts`);
|
|
114
|
+
projectProblems++;
|
|
115
|
+
}
|
|
116
|
+
if (projectProblems === 0) {
|
|
117
|
+
console.log(`✓ ${project.label}: wrangler config`);
|
|
118
|
+
}
|
|
119
|
+
problems += projectProblems;
|
|
120
|
+
}
|
|
121
|
+
return problems;
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import fc from 'fast-check';
|
|
3
|
+
import {
|
|
4
|
+
extractInlineLocalBlocksForTest,
|
|
5
|
+
INLINE_LOCAL_BEGIN,
|
|
6
|
+
INLINE_LOCAL_END,
|
|
7
|
+
LOCAL_SECTION_MARKER,
|
|
8
|
+
reinsertInlineLocalBlocksForTest,
|
|
9
|
+
splitLocalSectionForTest,
|
|
10
|
+
} from './managed-files.js';
|
|
11
|
+
|
|
12
|
+
const MANAGED = '# managed content\npath merge=driver\n';
|
|
13
|
+
|
|
14
|
+
describe('managed-file local sections', () => {
|
|
15
|
+
it('content without a marker is entirely managed', () => {
|
|
16
|
+
const { managed, localTail } = splitLocalSectionForTest(MANAGED);
|
|
17
|
+
expect(managed).toBe(MANAGED);
|
|
18
|
+
expect(localTail).toBe('');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('everything from the marker onward is the repo-owned tail', () => {
|
|
22
|
+
const tail = `${LOCAL_SECTION_MARKER}\ncustom/*.jsonl merge=custom-log\n`;
|
|
23
|
+
const { managed, localTail } = splitLocalSectionForTest(`${MANAGED}\n${tail}`);
|
|
24
|
+
expect(managed).toBe(`${MANAGED}\n`);
|
|
25
|
+
expect(localTail).toBe(tail);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('a tail directly after the managed content tolerates the separating newline', () => {
|
|
29
|
+
// The compare rule accepts `managed === content + '\n'` when a tail exists,
|
|
30
|
+
// so update → check round-trips as unchanged.
|
|
31
|
+
const written = `${MANAGED}\n${LOCAL_SECTION_MARKER}\nextra\n`;
|
|
32
|
+
const { managed, localTail } = splitLocalSectionForTest(written);
|
|
33
|
+
expect(managed).toBe(`${MANAGED}\n`);
|
|
34
|
+
expect(localTail.startsWith(LOCAL_SECTION_MARKER)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('managed-file inline local blocks', () => {
|
|
39
|
+
it('content with no inline markers extracts as fully managed, no blocks', () => {
|
|
40
|
+
const { withoutInline, blocks } = extractInlineLocalBlocksForTest(MANAGED);
|
|
41
|
+
expect(withoutInline).toBe(MANAGED);
|
|
42
|
+
expect(blocks).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('a wrapped block is extracted and removed, anchored on the preceding line', () => {
|
|
46
|
+
const current = ['a:', ' - one', ' - two', INLINE_LOCAL_BEGIN, ' - repo-owned', INLINE_LOCAL_END, 'b:'].join(
|
|
47
|
+
'\n',
|
|
48
|
+
);
|
|
49
|
+
const { withoutInline, blocks } = extractInlineLocalBlocksForTest(current);
|
|
50
|
+
expect(withoutInline).toBe(['a:', ' - one', ' - two', 'b:'].join('\n'));
|
|
51
|
+
expect(blocks).toEqual([{ anchor: ' - two', lines: ' - repo-owned' }]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('multiple blocks each anchor to their own preceding line', () => {
|
|
55
|
+
const current = [
|
|
56
|
+
'x',
|
|
57
|
+
INLINE_LOCAL_BEGIN,
|
|
58
|
+
'first',
|
|
59
|
+
INLINE_LOCAL_END,
|
|
60
|
+
'y',
|
|
61
|
+
INLINE_LOCAL_BEGIN,
|
|
62
|
+
'second',
|
|
63
|
+
INLINE_LOCAL_END,
|
|
64
|
+
'z',
|
|
65
|
+
].join('\n');
|
|
66
|
+
const { withoutInline, blocks } = extractInlineLocalBlocksForTest(current);
|
|
67
|
+
expect(withoutInline).toBe(['x', 'y', 'z'].join('\n'));
|
|
68
|
+
expect(blocks).toEqual([
|
|
69
|
+
{ anchor: 'x', lines: 'first' },
|
|
70
|
+
{ anchor: 'y', lines: 'second' },
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('a begin marker with no preceding line throws rather than silently dropping content', () => {
|
|
75
|
+
const current = [INLINE_LOCAL_BEGIN, 'orphan', INLINE_LOCAL_END].join('\n');
|
|
76
|
+
expect(() => extractInlineLocalBlocksForTest(current)).toThrow(/no preceding anchor/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('an unterminated begin marker throws rather than silently dropping content', () => {
|
|
80
|
+
const current = ['a', INLINE_LOCAL_BEGIN, 'unterminated'].join('\n');
|
|
81
|
+
expect(() => extractInlineLocalBlocksForTest(current)).toThrow(/no matching/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reinserting into fresh content with no blocks is a no-op', () => {
|
|
85
|
+
expect(reinsertInlineLocalBlocksForTest(MANAGED, [])).toBe(MANAGED);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reinserting splices the block back after its anchor in freshly rendered content', () => {
|
|
89
|
+
const fresh = ['a:', ' - one', ' - two', 'b:'].join('\n');
|
|
90
|
+
const result = reinsertInlineLocalBlocksForTest(fresh, [{ anchor: ' - two', lines: ' - repo-owned' }]);
|
|
91
|
+
expect(result).toBe(
|
|
92
|
+
['a:', ' - one', ' - two', INLINE_LOCAL_BEGIN, ' - repo-owned', INLINE_LOCAL_END, 'b:'].join('\n'),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reinserting refuses when the anchor no longer appears — never silently drops the repo's customization", () => {
|
|
97
|
+
const fresh = ['a:', ' - one', 'b:'].join('\n'); // ' - two' is gone
|
|
98
|
+
expect(() => reinsertInlineLocalBlocksForTest(fresh, [{ anchor: ' - two', lines: ' - repo-owned' }])).toThrow(
|
|
99
|
+
/no longer matches/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('property: extract then reinsert round-trips to the original for unique, single-occurrence anchors', () => {
|
|
104
|
+
// Lines that can serve as anchors: non-empty, not a marker, and each drawn
|
|
105
|
+
// from a small alphabet so uniqueness is checkable — the round-trip only
|
|
106
|
+
// holds when an anchor line occurs exactly once in the managed section
|
|
107
|
+
// (reinsert finds the FIRST occurrence by design, same as the real file
|
|
108
|
+
// shape: comments/patterns are unique in practice).
|
|
109
|
+
const linePool = fc.constantFrom('alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta');
|
|
110
|
+
fc.assert(
|
|
111
|
+
fc.property(
|
|
112
|
+
fc.uniqueArray(linePool, { minLength: 1, maxLength: 6 }),
|
|
113
|
+
fc.array(fc.string(), { maxLength: 3 }),
|
|
114
|
+
(anchors, blockLinesFlat) => {
|
|
115
|
+
const blockLines = blockLinesFlat.length > 0 ? [blockLinesFlat.join('|') || 'x'] : ['x'];
|
|
116
|
+
const fresh = anchors.join('\n');
|
|
117
|
+
const withBlocks = anchors
|
|
118
|
+
.map((anchor) => [anchor, INLINE_LOCAL_BEGIN, ...blockLines, INLINE_LOCAL_END].join('\n'))
|
|
119
|
+
.join('\n');
|
|
120
|
+
const { withoutInline, blocks } = extractInlineLocalBlocksForTest(withBlocks);
|
|
121
|
+
expect(withoutInline).toBe(fresh);
|
|
122
|
+
const reinserted = reinsertInlineLocalBlocksForTest(fresh, blocks);
|
|
123
|
+
expect(reinserted).toBe(withBlocks);
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -8,6 +8,14 @@ import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
|
8
8
|
|
|
9
9
|
type ManagedKind = 'raw' | 'template' | 'generated';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Repos may append their own content to a managed file below this marker —
|
|
13
|
+
* e.g. extra merge drivers in .gitattributes. Everything from the marker
|
|
14
|
+
* line onward is preserved verbatim across updates and ignored by the
|
|
15
|
+
* drift check; the managed section above it stays byte-exact.
|
|
16
|
+
*/
|
|
17
|
+
export const LOCAL_SECTION_MARKER = '# smoo-local: everything below this line is repo-owned and preserved';
|
|
18
|
+
|
|
11
19
|
interface ManagedFile {
|
|
12
20
|
kind: ManagedKind;
|
|
13
21
|
source: string;
|
|
@@ -16,6 +24,92 @@ interface ManagedFile {
|
|
|
16
24
|
releasePackagesOnly?: boolean;
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/** Split a managed target's content into the managed part and the repo-owned tail. */
|
|
28
|
+
function splitLocalSection(current: string): { managed: string; localTail: string } {
|
|
29
|
+
const index = current.indexOf(LOCAL_SECTION_MARKER);
|
|
30
|
+
if (index === -1) return { managed: current, localTail: '' };
|
|
31
|
+
return { managed: current.slice(0, index), localTail: current.slice(index) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Test seam for the pure splitter. */
|
|
35
|
+
export const splitLocalSectionForTest = splitLocalSection;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A repo-owned block INSIDE the managed section — e.g. one extra pattern
|
|
39
|
+
* spliced into a formatter's list, where a trailing marker (LOCAL_SECTION_MARKER)
|
|
40
|
+
* can't express it because it isn't at the end of the file. Wrap it in
|
|
41
|
+
* `# smoo-local-begin` / `# smoo-local-end`; the block is anchored to the line
|
|
42
|
+
* immediately before `# smoo-local-begin`. On update, the block is re-spliced
|
|
43
|
+
* right after that same anchor line in the freshly rendered template — if the
|
|
44
|
+
* anchor no longer appears there (the template reworked that section), the
|
|
45
|
+
* update refuses rather than silently dropping the repo's customization.
|
|
46
|
+
*/
|
|
47
|
+
export const INLINE_LOCAL_BEGIN = '# smoo-local-begin';
|
|
48
|
+
export const INLINE_LOCAL_END = '# smoo-local-end';
|
|
49
|
+
|
|
50
|
+
interface InlineLocalBlock {
|
|
51
|
+
anchor: string;
|
|
52
|
+
lines: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Pull inline local blocks out of a managed section, returning the section
|
|
56
|
+
* with each block (and its markers) removed, plus the extracted blocks in
|
|
57
|
+
* the order they appeared. */
|
|
58
|
+
function extractInlineLocalBlocks(managed: string): { withoutInline: string; blocks: InlineLocalBlock[] } {
|
|
59
|
+
const lines = managed.split('\n');
|
|
60
|
+
const kept: string[] = [];
|
|
61
|
+
const blocks: InlineLocalBlock[] = [];
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
if (line !== undefined && line.trim() === INLINE_LOCAL_BEGIN) {
|
|
66
|
+
const anchor = kept.at(-1);
|
|
67
|
+
if (anchor === undefined) {
|
|
68
|
+
throw new Error(`${INLINE_LOCAL_BEGIN} on line ${i + 1} has no preceding anchor line`);
|
|
69
|
+
}
|
|
70
|
+
const blockLines: string[] = [];
|
|
71
|
+
i += 1;
|
|
72
|
+
while (i < lines.length && lines[i]?.trim() !== INLINE_LOCAL_END) {
|
|
73
|
+
blockLines.push(lines[i] as string);
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
if (i >= lines.length) {
|
|
77
|
+
throw new Error(`${INLINE_LOCAL_BEGIN} anchored on "${anchor}" has no matching ${INLINE_LOCAL_END}`);
|
|
78
|
+
}
|
|
79
|
+
blocks.push({ anchor, lines: blockLines.join('\n') });
|
|
80
|
+
i += 1; // skip the END marker line itself
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
kept.push(line);
|
|
84
|
+
i += 1;
|
|
85
|
+
}
|
|
86
|
+
return { withoutInline: kept.join('\n'), blocks };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Test seam for the pure extractor. */
|
|
90
|
+
export const extractInlineLocalBlocksForTest = extractInlineLocalBlocks;
|
|
91
|
+
|
|
92
|
+
/** Re-splice extracted inline blocks into freshly rendered managed content,
|
|
93
|
+
* each immediately after its anchor line. A no-op when there are no blocks. */
|
|
94
|
+
function reinsertInlineLocalBlocks(content: string, blocks: InlineLocalBlock[]): string {
|
|
95
|
+
if (blocks.length === 0) return content;
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
for (const block of blocks) {
|
|
98
|
+
const index = lines.indexOf(block.anchor);
|
|
99
|
+
if (index === -1) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${INLINE_LOCAL_BEGIN} block anchored on "${block.anchor}" no longer matches any line in the updated ` +
|
|
102
|
+
'template — reconcile the repo-owned block manually',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
lines.splice(index + 1, 0, INLINE_LOCAL_BEGIN, ...block.lines.split('\n'), INLINE_LOCAL_END);
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Test seam for the pure re-splicer. */
|
|
111
|
+
export const reinsertInlineLocalBlocksForTest = reinsertInlineLocalBlocks;
|
|
112
|
+
|
|
19
113
|
export interface FileResult {
|
|
20
114
|
target: string;
|
|
21
115
|
action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
|
|
@@ -154,13 +248,17 @@ function applyManagedFile(
|
|
|
154
248
|
throw new Error(`${file.target} exists but is not a regular file or symlink`);
|
|
155
249
|
}
|
|
156
250
|
const current = readFileSync(target, 'utf8');
|
|
157
|
-
|
|
251
|
+
const { managed, localTail } = splitLocalSection(current);
|
|
252
|
+
const { withoutInline, blocks } = extractInlineLocalBlocks(managed);
|
|
253
|
+
if (withoutInline === content || (localTail !== '' && withoutInline === `${content}\n`)) {
|
|
158
254
|
return { target: file.target, action: 'unchanged' };
|
|
159
255
|
}
|
|
160
256
|
if (mode === 'check' || mode === 'diff') {
|
|
161
257
|
return { target: file.target, action: 'drifted' };
|
|
162
258
|
}
|
|
163
|
-
|
|
259
|
+
const rendered = reinsertInlineLocalBlocks(content, blocks);
|
|
260
|
+
const next = localTail === '' ? rendered : `${rendered}\n${localTail}`;
|
|
261
|
+
writeManagedFile(target, next, file.executable === true);
|
|
164
262
|
return { target: file.target, action: 'updated' };
|
|
165
263
|
}
|
|
166
264
|
if (mode === 'check' || mode === 'diff') {
|
|
@@ -90,7 +90,7 @@ async function validatePackedManifest(
|
|
|
90
90
|
async function validateAttw(root: string, pkg: PackageInfo, packed: { path: string }): Promise<number> {
|
|
91
91
|
const attw = await loadAttwCore();
|
|
92
92
|
const analysis = await attw.checkPackage(await createAttwPackageFromTarball(attw, root, packed.path), {
|
|
93
|
-
excludeEntrypoints:
|
|
93
|
+
excludeEntrypoints: nonJsExportEntrypoints(pkg.json.exports),
|
|
94
94
|
});
|
|
95
95
|
if (!isRecord(analysis) || analysis.types === false) {
|
|
96
96
|
return 0;
|
|
@@ -242,20 +242,22 @@ function formatResolutionKind(kind: string): string {
|
|
|
242
242
|
return kind;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
function
|
|
245
|
+
function nonJsExportEntrypoints(exports: unknown): string[] {
|
|
246
246
|
if (!isRecord(exports)) {
|
|
247
247
|
return [];
|
|
248
248
|
}
|
|
249
249
|
return Object.entries(exports)
|
|
250
|
-
.filter(([key, value]) => key.startsWith('.') &&
|
|
250
|
+
.filter(([key, value]) => key.startsWith('.') && exportPointsToNonJs(value))
|
|
251
251
|
.map(([key]) => key);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
function
|
|
254
|
+
function exportPointsToNonJs(value: unknown): boolean {
|
|
255
255
|
if (typeof value === 'string') {
|
|
256
|
-
|
|
256
|
+
// attw resolves every entrypoint as a module; assets can never have types.
|
|
257
|
+
// Existence of the target files is still validated by publint.
|
|
258
|
+
return value.endsWith('.wasm') || value.endsWith('.css');
|
|
257
259
|
}
|
|
258
|
-
return isRecord(value) && Object.values(value).some(
|
|
260
|
+
return isRecord(value) && Object.values(value).some(exportPointsToNonJs);
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
async function packPackage(root: string, pkg: PackageInfo): Promise<{ path: string; arrayBuffer: ArrayBuffer }> {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from '../packed-package.js';
|
|
27
27
|
import { syncRootRuntimeVersions } from '../runtime.js';
|
|
28
28
|
import { applyToolConfigDefaults, validateToolConfig } from '../tool-validation.js';
|
|
29
|
+
import { applyWranglerDefaults, validateWrangler } from '../wrangler.js';
|
|
29
30
|
|
|
30
31
|
export interface MonorepoContext {
|
|
31
32
|
root: string;
|
|
@@ -150,6 +151,18 @@ const packs: MonorepoPack[] = [
|
|
|
150
151
|
return validatePackedPublicPackageTypes(ctx.root);
|
|
151
152
|
},
|
|
152
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: 'wrangler',
|
|
156
|
+
init(ctx) {
|
|
157
|
+
applyWranglerDefaults(ctx.root);
|
|
158
|
+
},
|
|
159
|
+
fixPreBuild(ctx) {
|
|
160
|
+
applyWranglerDefaults(ctx.root);
|
|
161
|
+
},
|
|
162
|
+
validatePreBuild(ctx) {
|
|
163
|
+
return validateWrangler(ctx.root);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
153
166
|
];
|
|
154
167
|
|
|
155
168
|
export async function runInitPacks(ctx: MonorepoContext): Promise<void> {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it, spyOn } from 'bun:test';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { applyWranglerDefaults, firstWranglerEnv, validateWrangler } from './wrangler.js';
|
|
6
|
+
|
|
7
|
+
describe('firstWranglerEnv', () => {
|
|
8
|
+
it('extracts the first [env.<name>] header', () => {
|
|
9
|
+
expect(firstWranglerEnv('name = "svc"\n\n[env.staging]\nvars = {}\n\n[env.production]\n')).toBe('staging');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('handles nested env sub-tables', () => {
|
|
13
|
+
expect(firstWranglerEnv('[env.production.vars]\nFOO = "bar"\n')).toBe('production');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns null when the toml declares no env block', () => {
|
|
17
|
+
expect(firstWranglerEnv('name = "svc"\nmain = "src/index.ts"\n')).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('applyWranglerDefaults', () => {
|
|
22
|
+
it('injects the wrangler-types target and wires typecheck, idempotently', async () => {
|
|
23
|
+
const root = await createWorkspace([
|
|
24
|
+
{ dir: 'api', name: '@acme/api', toml: '[env.production]\n', nx: { targets: { typecheck: { dependsOn: ['^build'] } } } },
|
|
25
|
+
]);
|
|
26
|
+
try {
|
|
27
|
+
applyWranglerDefaults(root);
|
|
28
|
+
const pkg = await readJson(join(root, 'packages/api/package.json'));
|
|
29
|
+
expect(nxTargets(pkg)['wrangler-types']).toEqual({
|
|
30
|
+
executor: 'nx:run-commands',
|
|
31
|
+
cache: true,
|
|
32
|
+
inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
|
|
33
|
+
outputs: ['{projectRoot}/worker-configuration.d.ts'],
|
|
34
|
+
options: {
|
|
35
|
+
command: 'wrangler types --env production --env-file .dev.vars.example --include-runtime false',
|
|
36
|
+
cwd: '{projectRoot}',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const typecheck = nxTargets(pkg).typecheck;
|
|
40
|
+
expect(isRecord(typecheck) ? typecheck.dependsOn : null).toEqual(['^build', 'wrangler-types']);
|
|
41
|
+
|
|
42
|
+
const logs = captureConsoleLogs();
|
|
43
|
+
applyWranglerDefaults(root);
|
|
44
|
+
expect(logs.some((line) => line.startsWith('updated'))).toBe(false);
|
|
45
|
+
expect(logs.some((line) => line.startsWith('unchanged'))).toBe(true);
|
|
46
|
+
expect(await readJson(join(root, 'packages/api/package.json'))).toEqual(pkg);
|
|
47
|
+
} finally {
|
|
48
|
+
console.log = originalConsoleLog;
|
|
49
|
+
await rm(root, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('omits --env from the command when the wrangler.toml declares no env block', async () => {
|
|
54
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\nmain = "src/index.ts"\n' }]);
|
|
55
|
+
captureConsoleLogs();
|
|
56
|
+
try {
|
|
57
|
+
applyWranglerDefaults(root);
|
|
58
|
+
const target = nxTargets(await readJson(join(root, 'packages/api/package.json')))['wrangler-types'];
|
|
59
|
+
const options = isRecord(target) ? target.options : null;
|
|
60
|
+
expect(isRecord(options) ? options.command : null).toBe(
|
|
61
|
+
'wrangler types --env-file .dev.vars.example --include-runtime false',
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
console.log = originalConsoleLog;
|
|
65
|
+
await rm(root, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('creates an empty .dev.vars.example and logs it when the project has none', async () => {
|
|
70
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
|
|
71
|
+
const logs = captureConsoleLogs();
|
|
72
|
+
try {
|
|
73
|
+
applyWranglerDefaults(root);
|
|
74
|
+
expect(await readFile(join(root, 'packages/api/.dev.vars.example'), 'utf8')).toBe('');
|
|
75
|
+
expect(logs.some((line) => line.startsWith('created') && line.includes('api/.dev.vars.example'))).toBe(true);
|
|
76
|
+
} finally {
|
|
77
|
+
console.log = originalConsoleLog;
|
|
78
|
+
await rm(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('preserves a human-authored .dev.vars.example and does not log a creation', async () => {
|
|
83
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
|
|
84
|
+
const examplePath = join(root, 'packages/api/.dev.vars.example');
|
|
85
|
+
await writeFile(examplePath, 'GITHUB_CLIENT_SECRET=\n');
|
|
86
|
+
const logs = captureConsoleLogs();
|
|
87
|
+
try {
|
|
88
|
+
applyWranglerDefaults(root);
|
|
89
|
+
expect(await readFile(examplePath, 'utf8')).toBe('GITHUB_CLIENT_SECRET=\n');
|
|
90
|
+
expect(logs.some((line) => line.startsWith('created') && line.includes('.dev.vars.example'))).toBe(false);
|
|
91
|
+
} finally {
|
|
92
|
+
console.log = originalConsoleLog;
|
|
93
|
+
await rm(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('adds the missing entries to an incomplete root .gitignore', async () => {
|
|
98
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
|
|
99
|
+
const logs = captureConsoleLogs();
|
|
100
|
+
try {
|
|
101
|
+
await writeFile(join(root, '.gitignore'), 'node_modules\n');
|
|
102
|
+
applyWranglerDefaults(root);
|
|
103
|
+
const lines = (await readFile(join(root, '.gitignore'), 'utf8')).split('\n').map((line) => line.trim());
|
|
104
|
+
expect(lines).toContain('.dev.vars');
|
|
105
|
+
expect(lines).toContain('worker-configuration.d.ts');
|
|
106
|
+
expect(logs.some((line) => line.startsWith('updated') && line.includes('.gitignore'))).toBe(true);
|
|
107
|
+
} finally {
|
|
108
|
+
console.log = originalConsoleLog;
|
|
109
|
+
await rm(root, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('leaves the root .gitignore untouched when both entries are already present', async () => {
|
|
114
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\n' }]);
|
|
115
|
+
const gitignorePath = join(root, '.gitignore');
|
|
116
|
+
const logs = captureConsoleLogs();
|
|
117
|
+
try {
|
|
118
|
+
await writeFile(gitignorePath, 'node_modules\n.dev.vars\nworker-configuration.d.ts\n');
|
|
119
|
+
const before = await readFile(gitignorePath, 'utf8');
|
|
120
|
+
applyWranglerDefaults(root);
|
|
121
|
+
expect(await readFile(gitignorePath, 'utf8')).toBe(before);
|
|
122
|
+
expect(logs.some((line) => line.startsWith('updated') && line.includes('.gitignore'))).toBe(false);
|
|
123
|
+
} finally {
|
|
124
|
+
console.log = originalConsoleLog;
|
|
125
|
+
await rm(root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('validateWrangler', () => {
|
|
131
|
+
it('flags a project whose .dev.vars.example is missing while target and .gitignore are wired', async () => {
|
|
132
|
+
const root = await createWorkspace([
|
|
133
|
+
{
|
|
134
|
+
dir: 'api',
|
|
135
|
+
name: '@acme/api',
|
|
136
|
+
toml: '[env.staging]\n',
|
|
137
|
+
nx: { targets: { 'wrangler-types': { executor: 'nx:run-commands' } } },
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
try {
|
|
141
|
+
await writeFile(join(root, '.gitignore'), '.dev.vars\nworker-configuration.d.ts\n');
|
|
142
|
+
expect(validateWrangler(root)).toBe(1);
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(root, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('passes a fully wired project', async () => {
|
|
149
|
+
const root = await createWorkspace([{ dir: 'api', name: '@acme/api', toml: '[env.staging]\n' }]);
|
|
150
|
+
try {
|
|
151
|
+
applyWranglerDefaults(root);
|
|
152
|
+
await writeFile(join(root, 'packages/api/.dev.vars.example'), 'SECRET=\n');
|
|
153
|
+
await writeFile(join(root, '.gitignore'), '.dev.vars\n!.dev.vars.example\nworker-configuration.d.ts\n');
|
|
154
|
+
expect(validateWrangler(root)).toBe(0);
|
|
155
|
+
} finally {
|
|
156
|
+
await rm(root, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
async function createWorkspace(
|
|
162
|
+
packages: Array<{ dir: string; name: string; toml: string; nx?: Record<string, unknown> }>,
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-wrangler-'));
|
|
165
|
+
await writeJson(join(root, 'package.json'), {
|
|
166
|
+
name: '@acme/codebase',
|
|
167
|
+
version: '0.0.0',
|
|
168
|
+
private: true,
|
|
169
|
+
workspaces: ['packages/*'],
|
|
170
|
+
});
|
|
171
|
+
for (const pkg of packages) {
|
|
172
|
+
await writeJson(join(root, `packages/${pkg.dir}/package.json`), {
|
|
173
|
+
name: pkg.name,
|
|
174
|
+
version: '0.0.0',
|
|
175
|
+
private: true,
|
|
176
|
+
...(pkg.nx ? { nx: pkg.nx } : {}),
|
|
177
|
+
});
|
|
178
|
+
await writeFile(join(root, `packages/${pkg.dir}/wrangler.toml`), pkg.toml);
|
|
179
|
+
}
|
|
180
|
+
return root;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function nxTargets(pkg: Record<string, unknown>): Record<string, unknown> {
|
|
184
|
+
const nx = pkg.nx;
|
|
185
|
+
if (!isRecord(nx) || !isRecord(nx.targets)) {
|
|
186
|
+
throw new Error('nx.targets not found');
|
|
187
|
+
}
|
|
188
|
+
return nx.targets;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
192
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readJson(path: string): Promise<Record<string, unknown>> {
|
|
196
|
+
const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
|
|
197
|
+
if (!isRecord(parsed)) {
|
|
198
|
+
throw new Error('expected JSON object');
|
|
199
|
+
}
|
|
200
|
+
return parsed;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
|
|
204
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
205
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const originalConsoleLog = console.log;
|
|
209
|
+
|
|
210
|
+
function captureConsoleLogs(): string[] {
|
|
211
|
+
const logs: string[] = [];
|
|
212
|
+
spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
213
|
+
logs.push(args.join(' '));
|
|
214
|
+
});
|
|
215
|
+
return logs;
|
|
216
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
|
|
4
|
+
import { getWorkspacePackageManifests } from '../lib/workspace.js';
|
|
5
|
+
|
|
6
|
+
interface WranglerProject {
|
|
7
|
+
label: string;
|
|
8
|
+
dir: string;
|
|
9
|
+
packageJsonPath: string;
|
|
10
|
+
json: Record<string, unknown>;
|
|
11
|
+
tomlPath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** First `[env.<name>]` header in a wrangler.toml, or `null` when the config declares no envs (top-level bindings only). */
|
|
15
|
+
export function firstWranglerEnv(tomlText: string): string | null {
|
|
16
|
+
const match = tomlText.match(/^\s*\[env\.([A-Za-z0-9_-]+)/m);
|
|
17
|
+
return match ? match[1] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function wranglerProjects(root: string): WranglerProject[] {
|
|
21
|
+
const projects: WranglerProject[] = [];
|
|
22
|
+
for (const pkg of getWorkspacePackageManifests(root)) {
|
|
23
|
+
const tomlPath = join(pkg.path, 'wrangler.toml');
|
|
24
|
+
if (!existsSync(tomlPath)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
projects.push({
|
|
28
|
+
label: relative(root, pkg.path) || '.',
|
|
29
|
+
dir: pkg.path,
|
|
30
|
+
packageJsonPath: pkg.packageJsonPath,
|
|
31
|
+
json: pkg.json,
|
|
32
|
+
tomlPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return projects;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Ensure the root .gitignore ignores the local secret file + generated types (once). */
|
|
39
|
+
function ensureGitignore(root: string): void {
|
|
40
|
+
const path = join(root, '.gitignore');
|
|
41
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
42
|
+
const lines = text.split('\n').map((line) => line.trim());
|
|
43
|
+
const missing = ['.dev.vars', 'worker-configuration.d.ts'].filter((entry) => !lines.includes(entry));
|
|
44
|
+
if (missing.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const block = `${text.replace(/\s*$/, '')}\n\n# wrangler: local secret values + generated types (names live in .dev.vars.example)\n${missing.join('\n')}\n`;
|
|
48
|
+
writeFileSync(path, block);
|
|
49
|
+
console.log(`updated .gitignore (${missing.join(', ')})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function applyWranglerDefaults(root: string): void {
|
|
53
|
+
const projects = wranglerProjects(root);
|
|
54
|
+
if (projects.length > 0) {
|
|
55
|
+
ensureGitignore(root);
|
|
56
|
+
}
|
|
57
|
+
for (const project of projects) {
|
|
58
|
+
const env = firstWranglerEnv(readFileSync(project.tomlPath, 'utf8'));
|
|
59
|
+
const nx = getOrCreateRecord(project.json, 'nx');
|
|
60
|
+
const targets = getOrCreateRecord(nx, 'targets');
|
|
61
|
+
const desired: Record<string, unknown> = {
|
|
62
|
+
executor: 'nx:run-commands',
|
|
63
|
+
cache: true,
|
|
64
|
+
inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
|
|
65
|
+
outputs: ['{projectRoot}/worker-configuration.d.ts'],
|
|
66
|
+
options: {
|
|
67
|
+
command: `wrangler types${env ? ` --env ${env}` : ''} --env-file .dev.vars.example --include-runtime false`,
|
|
68
|
+
cwd: '{projectRoot}',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
let changed = false;
|
|
72
|
+
const existing = recordProperty(targets, 'wrangler-types');
|
|
73
|
+
if (!existing || JSON.stringify(existing) !== JSON.stringify(desired)) {
|
|
74
|
+
targets['wrangler-types'] = desired;
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
const typecheck = recordProperty(targets, 'typecheck');
|
|
78
|
+
if (typecheck) {
|
|
79
|
+
const dependsOn: unknown[] = Array.isArray(typecheck.dependsOn) ? typecheck.dependsOn : [];
|
|
80
|
+
if (!dependsOn.includes('wrangler-types')) {
|
|
81
|
+
dependsOn.push('wrangler-types');
|
|
82
|
+
typecheck.dependsOn = dependsOn;
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (changed) {
|
|
87
|
+
writeJsonObject(project.packageJsonPath, project.json);
|
|
88
|
+
console.log(`updated ${project.label}/package.json wrangler-types target`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`unchanged ${project.label}/package.json wrangler-types target`);
|
|
91
|
+
}
|
|
92
|
+
// Empty .dev.vars.example = "this worker declares no secrets". Bootstrap it
|
|
93
|
+
// when missing; humans add SECRET_NAME= lines if the worker reads any (the
|
|
94
|
+
// wrangler-types + tsc gate fails on an undeclared secret until they do).
|
|
95
|
+
const examplePath = join(project.dir, '.dev.vars.example');
|
|
96
|
+
if (!existsSync(examplePath)) {
|
|
97
|
+
writeFileSync(examplePath, '');
|
|
98
|
+
console.log(`created ${project.label}/.dev.vars.example (no secrets)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateWrangler(root: string): number {
|
|
104
|
+
const gitignorePath = join(root, '.gitignore');
|
|
105
|
+
const gitignoreLines = existsSync(gitignorePath)
|
|
106
|
+
? readFileSync(gitignorePath, 'utf8')
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => line.trim())
|
|
109
|
+
: [];
|
|
110
|
+
const gitignoreCoversSecrets =
|
|
111
|
+
gitignoreLines.includes('.dev.vars') && gitignoreLines.includes('worker-configuration.d.ts');
|
|
112
|
+
let problems = 0;
|
|
113
|
+
for (const project of wranglerProjects(root)) {
|
|
114
|
+
let projectProblems = 0;
|
|
115
|
+
if (!existsSync(join(project.dir, '.dev.vars.example'))) {
|
|
116
|
+
console.log(`⨯ ${project.label}: missing .dev.vars.example`);
|
|
117
|
+
projectProblems++;
|
|
118
|
+
}
|
|
119
|
+
const nx = recordProperty(project.json, 'nx');
|
|
120
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
121
|
+
if (!targets || !recordProperty(targets, 'wrangler-types')) {
|
|
122
|
+
console.log(`⨯ ${project.label}: missing wrangler-types nx target`);
|
|
123
|
+
projectProblems++;
|
|
124
|
+
}
|
|
125
|
+
if (!gitignoreCoversSecrets) {
|
|
126
|
+
console.log(`⨯ ${project.label}: root .gitignore must ignore .dev.vars and worker-configuration.d.ts`);
|
|
127
|
+
projectProblems++;
|
|
128
|
+
}
|
|
129
|
+
if (projectProblems === 0) {
|
|
130
|
+
console.log(`✓ ${project.label}: wrangler config`);
|
|
131
|
+
}
|
|
132
|
+
problems += projectProblems;
|
|
133
|
+
}
|
|
134
|
+
return problems;
|
|
135
|
+
}
|
package/src/pr/index.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ afterEach(() => {
|
|
|
18
18
|
|
|
19
19
|
const PR_JSON = JSON.stringify({
|
|
20
20
|
number: 40,
|
|
21
|
-
url: 'https://github.com/
|
|
21
|
+
url: 'https://github.com/acme/private/pull/40',
|
|
22
22
|
headRefName: 'gar-sync/private-to-public',
|
|
23
23
|
baseRefName: 'public-mirror',
|
|
24
24
|
isCrossRepository: false,
|
|
@@ -145,7 +145,7 @@ function writeState(overrides: Record<string, unknown>): void {
|
|
|
145
145
|
mkdirSync(join(gitDir, 'smoo'), { recursive: true });
|
|
146
146
|
const state = {
|
|
147
147
|
pr: 40,
|
|
148
|
-
url: 'https://github.com/
|
|
148
|
+
url: 'https://github.com/acme/private/pull/40',
|
|
149
149
|
headBranch: 'gar-sync/private-to-public',
|
|
150
150
|
baseBranch: 'public-mirror',
|
|
151
151
|
remote: 'origin',
|