@smoothbricks/cli 0.6.0 → 0.7.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.
@@ -1,3 +1,17 @@
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;
1
15
  export interface FileResult {
2
16
  target: string;
3
17
  action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
@@ -5,4 +19,5 @@ export interface FileResult {
5
19
  export declare function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[];
6
20
  export declare function printResults(results: FileResult[]): void;
7
21
  export declare function validateManagedFiles(root: string): number;
22
+ export {};
8
23
  //# 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":"AAkBA,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;AA4KD,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
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,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;AA8KD,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
@@ -5,6 +5,22 @@ 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;
8
24
  const managedFiles = [
9
25
  {
10
26
  kind: 'raw',
@@ -114,13 +130,15 @@ function applyManagedFile(root, file, mode, context) {
114
130
  throw new Error(`${file.target} exists but is not a regular file or symlink`);
115
131
  }
116
132
  const current = readFileSync(target, 'utf8');
117
- if (current === content) {
133
+ const { managed, localTail } = splitLocalSection(current);
134
+ if (managed === content || (localTail !== '' && managed === `${content}\n`)) {
118
135
  return { target: file.target, action: 'unchanged' };
119
136
  }
120
137
  if (mode === 'check' || mode === 'diff') {
121
138
  return { target: file.target, action: 'drifted' };
122
139
  }
123
- writeManagedFile(target, content, file.executable === true);
140
+ const next = localTail === '' ? content : `${content}\n${localTail}`;
141
+ writeManagedFile(target, next, file.executable === true);
124
142
  return { target: file.target, action: 'updated' };
125
143
  }
126
144
  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: wasmExportEntrypoints(pkg.json.exports),
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 wasmExportEntrypoints(exports) {
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('.') && exportPointsToWasm(value))
213
+ .filter(([key, value]) => key.startsWith('.') && exportPointsToNonJs(value))
214
214
  .map(([key]) => key);
215
215
  }
216
- function exportPointsToWasm(value) {
216
+ function exportPointsToNonJs(value) {
217
217
  if (typeof value === 'string') {
218
- return value.endsWith('.wasm');
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(exportPointsToWasm);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { LOCAL_SECTION_MARKER, splitLocalSectionForTest } from './managed-files.js';
3
+
4
+ const MANAGED = '# managed content\npath merge=driver\n';
5
+
6
+ describe('managed-file local sections', () => {
7
+ it('content without a marker is entirely managed', () => {
8
+ const { managed, localTail } = splitLocalSectionForTest(MANAGED);
9
+ expect(managed).toBe(MANAGED);
10
+ expect(localTail).toBe('');
11
+ });
12
+
13
+ it('everything from the marker onward is the repo-owned tail', () => {
14
+ const tail = `${LOCAL_SECTION_MARKER}\ncustom/*.jsonl merge=custom-log\n`;
15
+ const { managed, localTail } = splitLocalSectionForTest(`${MANAGED}\n${tail}`);
16
+ expect(managed).toBe(`${MANAGED}\n`);
17
+ expect(localTail).toBe(tail);
18
+ });
19
+
20
+ it('a tail directly after the managed content tolerates the separating newline', () => {
21
+ // The compare rule accepts `managed === content + '\n'` when a tail exists,
22
+ // so update → check round-trips as unchanged.
23
+ const written = `${MANAGED}\n${LOCAL_SECTION_MARKER}\nextra\n`;
24
+ const { managed, localTail } = splitLocalSectionForTest(written);
25
+ expect(managed).toBe(`${MANAGED}\n`);
26
+ expect(localTail.startsWith(LOCAL_SECTION_MARKER)).toBe(true);
27
+ });
28
+ });
@@ -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,16 @@ 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
+
19
37
  export interface FileResult {
20
38
  target: string;
21
39
  action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
@@ -154,13 +172,15 @@ function applyManagedFile(
154
172
  throw new Error(`${file.target} exists but is not a regular file or symlink`);
155
173
  }
156
174
  const current = readFileSync(target, 'utf8');
157
- if (current === content) {
175
+ const { managed, localTail } = splitLocalSection(current);
176
+ if (managed === content || (localTail !== '' && managed === `${content}\n`)) {
158
177
  return { target: file.target, action: 'unchanged' };
159
178
  }
160
179
  if (mode === 'check' || mode === 'diff') {
161
180
  return { target: file.target, action: 'drifted' };
162
181
  }
163
- writeManagedFile(target, content, file.executable === true);
182
+ const next = localTail === '' ? content : `${content}\n${localTail}`;
183
+ writeManagedFile(target, next, file.executable === true);
164
184
  return { target: file.target, action: 'updated' };
165
185
  }
166
186
  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: wasmExportEntrypoints(pkg.json.exports),
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 wasmExportEntrypoints(exports: unknown): string[] {
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('.') && exportPointsToWasm(value))
250
+ .filter(([key, value]) => key.startsWith('.') && exportPointsToNonJs(value))
251
251
  .map(([key]) => key);
252
252
  }
253
253
 
254
- function exportPointsToWasm(value: unknown): boolean {
254
+ function exportPointsToNonJs(value: unknown): boolean {
255
255
  if (typeof value === 'string') {
256
- return value.endsWith('.wasm');
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(exportPointsToWasm);
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 }> {
@@ -18,7 +18,7 @@ afterEach(() => {
18
18
 
19
19
  const PR_JSON = JSON.stringify({
20
20
  number: 40,
21
- url: 'https://github.com/conloca/private/pull/40',
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/conloca/private/pull/40',
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',