facebook / facebook/astryx

swizzle silently drops nested subdirectories, producing a broken partial eject (e.g. Table/plugins)

Open
#3,506 1 comment 1 reaction 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
13k
Forks
1.1k
Avg merge
1d 15h
Merged PRs (30d)
690

Description

## Summary

`astryx swizzle ` copies only the **top-level files** of a component directory and silently skips nested subdirectories. For components whose source is split across subfolders (e.g. `Table/plugins/*`), this produces an **incomplete and broken eject** — the copied entry file still imports from the dropped subdirectory, so the swizzled component fails module resolution — yet the CLI reports success. This contradicts the documented promise that swizzle "ejects a component's full source into your project to own."

## Root cause

`packages/cli/src/commands/swizzle.mjs` — the copy loop reads the component directory non-recursively and skips anything that isn't a file:

```js
// packages/cli/src/commands/swizzle.mjs (~L340)
const files = fs.readdirSync(componentDir);
for (const file of files) {
if (isExcludedFromCopy(file)) continue;
const srcPath = path.join(componentDir, file);
const stat = fs.statSync(srcPath);
if (!stat.isFile()) continue; // <-- subdirectories are silently dropped, no recursion
...
}
```

The pre-flight collision check (~L304) and the reported `copiedFiles` list (~L363) apply the same `stat.isFile()` filter, so nested files are invisible end-to-end — including in the success count.

Compounding it: `rewriteImports()` only rewrites **upward** relative imports (`/^\.\.\//`). A component's own **downward** imports into its subfolders (`./plugins/...`) are left untouched — now pointing at a directory that was never copied.

## Reproduction

```bash
node packages/cli/bin/astryx.mjs swizzle Table --out ./swizzled
```

`Table` has a `plugins/` subdirectory. After swizzling, the ejected `Table/index.ts` still contains (unmodified):

```ts
export {useTableSelection} from './plugins/selection';
export {useTableSortable} from './plugins/sortable';
export {useTablePagination, paginateData} from './plugins/pagination';
// ...15+ more re-exports from ./plugins/*
```

…but `./plugins/` does not exist in the output. The swizzled component fails to resolve on import/build, while the CLI reports a successful copy.

## Impact

- **Broken output, reported as success.** No error, no warning — the failure only surfaces later when the consumer tries to build/import the swizzled component.
- **Undermines a headline feature.** "Open internals / own the source" is a core selling point; it silently doesn't hold for any multi-directory component.
- Confirmed affected: `Table` (`plugins/`). Other core components ship nested source dirs too (`Calendar`, `FormLayout`) and are candidates for the same failure; any current or future component that organizes source into subfolders is at risk.

## Suggested fix

Recurse into subdirectories when copying (preserving relative structure under the output dir), and keep the collision check + reported file list consistent with the recursive walk. `rewriteImports` should continue to leave intra-component `./`-relative paths intact once the subtree is actually copied.

Alternatively, if partial ejects are ever intentional, `swizzle` should at minimum **detect and refuse/warn** when a component directory contains subdirectories it isn't copying, rather than reporting success.

---

*Filed after an architecture review of the repo at current `main`. Happy to open a PR for the recursive-copy fix if that's useful.*

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.