stackpatch 1.1.0 โ†’ 1.1.2

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.
Files changed (2) hide show
  1. package/bin/stackpatch.ts +182 -41
  2. package/package.json +1 -1
package/bin/stackpatch.ts CHANGED
@@ -303,53 +303,56 @@ async function copyFiles(src: string, dest: string): Promise<{ success: boolean;
303
303
  }
304
304
  }
305
305
 
306
+ // Track files from SOURCE (boilerplate) before copying
307
+ // This ensures we only track files that are actually from StackPatch
308
+ function trackSourceFiles(srcDir: string, baseDir: string, targetBase: string): void {
309
+ if (!fs.existsSync(srcDir)) return;
310
+
311
+ const files = fs.readdirSync(srcDir, { withFileTypes: true });
312
+ for (const file of files) {
313
+ const srcFilePath = path.join(srcDir, file.name);
314
+ if (file.isDirectory()) {
315
+ trackSourceFiles(srcFilePath, baseDir, targetBase);
316
+ } else {
317
+ const relativePath = path.relative(baseDir, srcFilePath);
318
+ const targetPath = targetBase
319
+ ? path.join(targetBase, relativePath).replace(/\\/g, "/")
320
+ : relativePath.replace(/\\/g, "/");
321
+ addedFiles.push(targetPath);
322
+ }
323
+ }
324
+ }
325
+
306
326
  // Copy files with smart app directory handling
307
327
  for (const entry of entries) {
308
328
  const srcPath = path.join(src, entry.name);
309
329
 
310
330
  if (entry.name === "app") {
331
+ // Track files from SOURCE boilerplate before copying
332
+ trackSourceFiles(srcPath, srcPath, appDir);
333
+
311
334
  // Copy app directory contents to the detected app directory location
312
335
  await fse.ensureDir(appDirPath);
313
336
  await fse.copy(srcPath, appDirPath, { overwrite: true });
314
-
315
- // Track all files in app directory
316
- function trackFiles(dir: string, baseDir: string) {
317
- const files = fs.readdirSync(dir, { withFileTypes: true });
318
- for (const file of files) {
319
- const filePath = path.join(dir, file.name);
320
- if (file.isDirectory()) {
321
- trackFiles(filePath, baseDir);
322
- } else {
323
- const relativePath = path.relative(baseDir, filePath);
324
- addedFiles.push(path.join(appDir, relativePath).replace(/\\/g, "/"));
325
- }
326
- }
327
- }
328
- trackFiles(appDirPath, appDirPath);
329
337
  } else if (entry.name === "components") {
338
+ // Track files from SOURCE boilerplate before copying
339
+ trackSourceFiles(srcPath, srcPath, componentsDir);
340
+
330
341
  // Copy components directory to the detected components directory location
331
342
  await fse.ensureDir(componentsDirPath);
332
343
  await fse.copy(srcPath, componentsDirPath, { overwrite: true });
333
-
334
- // Track all files in components directory
335
- function trackFiles(dir: string, baseDir: string) {
336
- const files = fs.readdirSync(dir, { withFileTypes: true });
337
- for (const file of files) {
338
- const filePath = path.join(dir, file.name);
339
- if (file.isDirectory()) {
340
- trackFiles(filePath, baseDir);
341
- } else {
342
- const relativePath = path.relative(baseDir, filePath);
343
- addedFiles.push(path.join(componentsDir, relativePath).replace(/\\/g, "/"));
344
- }
345
- }
346
- }
347
- trackFiles(componentsDirPath, componentsDirPath);
348
344
  } else {
345
+ // For root-level files/directories, track from source
346
+ const srcStat = fs.statSync(srcPath);
347
+ if (srcStat.isDirectory()) {
348
+ trackSourceFiles(srcPath, srcPath, "");
349
+ } else {
350
+ addedFiles.push(entry.name);
351
+ }
352
+
349
353
  // Copy other files/directories (middleware, etc.) to root
350
354
  const destPath = path.join(dest, entry.name);
351
355
  await fse.copy(srcPath, destPath, { overwrite: true });
352
- addedFiles.push(entry.name);
353
356
  }
354
357
  }
355
358
 
@@ -1790,9 +1793,66 @@ async function main() {
1790
1793
  let failedRestorations: string[] = [];
1791
1794
  const directoriesToCheck: Set<string> = new Set();
1792
1795
 
1793
- // Step 1: Remove added files
1796
+ // Step 1: Get list of valid StackPatch files from boilerplate
1797
+ // This ensures we only remove files that are actually from StackPatch
1798
+ const boilerplatePath = path.join(BOILERPLATE_ROOT, manifest.patchName === "auth-ui" ? "auth" : manifest.patchName);
1799
+ const validStackPatchFiles = new Set<string>();
1800
+
1801
+ function collectBoilerplateFiles(srcDir: string, baseDir: string, targetBase: string): void {
1802
+ if (!fs.existsSync(srcDir)) return;
1803
+
1804
+ const files = fs.readdirSync(srcDir, { withFileTypes: true });
1805
+ for (const file of files) {
1806
+ const srcFilePath = path.join(srcDir, file.name);
1807
+ if (file.isDirectory()) {
1808
+ collectBoilerplateFiles(srcFilePath, baseDir, targetBase);
1809
+ } else {
1810
+ const relativePath = path.relative(baseDir, srcFilePath);
1811
+ const targetPath = targetBase
1812
+ ? path.join(targetBase, relativePath).replace(/\\/g, "/")
1813
+ : relativePath.replace(/\\/g, "/");
1814
+ validStackPatchFiles.add(targetPath);
1815
+ }
1816
+ }
1817
+ }
1818
+
1819
+ // Collect files from boilerplate app directory
1820
+ const appDir = detectAppDirectory(target);
1821
+ const componentsDir = detectComponentsDirectory(target);
1822
+ const boilerplateAppPath = path.join(boilerplatePath, "app");
1823
+ const boilerplateComponentsPath = path.join(boilerplatePath, "components");
1824
+
1825
+ if (fs.existsSync(boilerplateAppPath)) {
1826
+ collectBoilerplateFiles(boilerplateAppPath, boilerplateAppPath, appDir);
1827
+ }
1828
+ if (fs.existsSync(boilerplateComponentsPath)) {
1829
+ collectBoilerplateFiles(boilerplateComponentsPath, boilerplateComponentsPath, componentsDir);
1830
+ }
1831
+
1832
+ // Collect root-level files
1833
+ if (fs.existsSync(boilerplatePath)) {
1834
+ const entries = fs.readdirSync(boilerplatePath, { withFileTypes: true });
1835
+ for (const entry of entries) {
1836
+ if (entry.name !== "app" && entry.name !== "components") {
1837
+ const srcPath = path.join(boilerplatePath, entry.name);
1838
+ if (entry.isDirectory()) {
1839
+ collectBoilerplateFiles(srcPath, srcPath, "");
1840
+ } else {
1841
+ validStackPatchFiles.add(entry.name);
1842
+ }
1843
+ }
1844
+ }
1845
+ }
1846
+
1847
+ // Step 1: Remove added files (only if they're actually from StackPatch boilerplate)
1794
1848
  console.log(chalk.white("๐Ÿ“ Removing added files..."));
1795
1849
  for (const filePath of manifest.files.added) {
1850
+ // Only remove if this file is actually in the boilerplate
1851
+ if (!validStackPatchFiles.has(filePath)) {
1852
+ console.log(chalk.gray(` โŠ˜ Skipped (not in boilerplate): ${filePath}`));
1853
+ continue;
1854
+ }
1855
+
1796
1856
  const fullPath = path.join(target, filePath);
1797
1857
  if (fs.existsSync(fullPath)) {
1798
1858
  try {
@@ -1851,24 +1911,95 @@ async function main() {
1851
1911
  }
1852
1912
  }
1853
1913
 
1854
- // Step 3: Restore modified files from backups
1914
+ // Step 3: Restore modified files from originalContent in manifest
1915
+ // This is more reliable than backups since it contains the true original content
1855
1916
  console.log(chalk.white("\n๐Ÿ“ Restoring modified files..."));
1856
1917
  for (const modified of manifest.files.modified) {
1857
- const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
1858
1918
  const originalPath = path.join(target, modified.path);
1859
1919
 
1860
- if (fs.existsSync(backupPath)) {
1920
+ if (modified.originalContent !== undefined) {
1861
1921
  try {
1862
- restoreFile(backupPath, originalPath);
1922
+ // Restore from originalContent in manifest (most reliable)
1923
+ const originalDir = path.dirname(originalPath);
1924
+ if (!fs.existsSync(originalDir)) {
1925
+ fs.mkdirSync(originalDir, { recursive: true });
1926
+ }
1927
+ fs.writeFileSync(originalPath, modified.originalContent, "utf-8");
1863
1928
  console.log(chalk.green(` โœ“ Restored: ${modified.path}`));
1864
1929
  restoredCount++;
1865
1930
  } catch (error) {
1866
- console.log(chalk.yellow(` โš  Could not restore: ${modified.path}`));
1867
- failedRestorations.push(modified.path);
1931
+ // Fallback to backup file if originalContent restore fails
1932
+ const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
1933
+ if (fs.existsSync(backupPath)) {
1934
+ try {
1935
+ restoreFile(backupPath, originalPath);
1936
+ console.log(chalk.green(` โœ“ Restored (from backup): ${modified.path}`));
1937
+ restoredCount++;
1938
+ } catch (backupError) {
1939
+ console.log(chalk.yellow(` โš  Could not restore: ${modified.path}`));
1940
+ failedRestorations.push(modified.path);
1941
+ }
1942
+ } else {
1943
+ console.log(chalk.yellow(` โš  Could not restore: ${modified.path} (no backup found)`));
1944
+ failedRestorations.push(modified.path);
1945
+ }
1868
1946
  }
1869
1947
  } else {
1870
- console.log(chalk.yellow(` โš  Backup not found: ${modified.path}`));
1871
- failedRestorations.push(modified.path);
1948
+ // Fallback: try to restore from backup file
1949
+ const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
1950
+ if (fs.existsSync(backupPath)) {
1951
+ try {
1952
+ restoreFile(backupPath, originalPath);
1953
+ console.log(chalk.green(` โœ“ Restored (from backup): ${modified.path}`));
1954
+ restoredCount++;
1955
+ } catch (error) {
1956
+ console.log(chalk.yellow(` โš  Could not restore: ${modified.path}`));
1957
+ failedRestorations.push(modified.path);
1958
+ }
1959
+ } else {
1960
+ console.log(chalk.yellow(` โš  Backup not found and no originalContent: ${modified.path}`));
1961
+ failedRestorations.push(modified.path);
1962
+ }
1963
+ }
1964
+
1965
+ // Safety check: If file still contains StackPatch components after restore, manually remove them
1966
+ if (fs.existsSync(originalPath) && modified.path.includes("layout.tsx")) {
1967
+ try {
1968
+ let content = fs.readFileSync(originalPath, "utf-8");
1969
+ let needsUpdate = false;
1970
+
1971
+ // Remove AuthSessionProvider import
1972
+ if (content.includes("AuthSessionProvider") && content.includes("session-provider")) {
1973
+ content = content.replace(/import\s*{\s*AuthSessionProvider\s*}\s*from\s*["'][^"']*session-provider[^"']*["'];\s*\n?/g, "");
1974
+ needsUpdate = true;
1975
+ }
1976
+
1977
+ // Remove Toaster import
1978
+ if (content.includes("Toaster") && content.includes("toaster")) {
1979
+ content = content.replace(/import\s*{\s*Toaster\s*}\s*from\s*["'][^"']*toaster[^"']*["'];\s*\n?/g, "");
1980
+ needsUpdate = true;
1981
+ }
1982
+
1983
+ // Remove AuthSessionProvider wrapper
1984
+ if (content.includes("<AuthSessionProvider>") && content.includes("</AuthSessionProvider>")) {
1985
+ content = content.replace(/<AuthSessionProvider>\s*/g, "");
1986
+ content = content.replace(/\s*<\/AuthSessionProvider>/g, "");
1987
+ needsUpdate = true;
1988
+ }
1989
+
1990
+ // Remove Toaster component
1991
+ if (content.includes("<Toaster")) {
1992
+ content = content.replace(/<Toaster\s*\/?>\s*\n?\s*/g, "");
1993
+ needsUpdate = true;
1994
+ }
1995
+
1996
+ if (needsUpdate) {
1997
+ fs.writeFileSync(originalPath, content, "utf-8");
1998
+ console.log(chalk.green(` โœ“ Cleaned up StackPatch components from: ${modified.path}`));
1999
+ }
2000
+ } catch (error) {
2001
+ // Ignore errors in cleanup
2002
+ }
1872
2003
  }
1873
2004
  }
1874
2005
 
@@ -1884,23 +2015,33 @@ async function main() {
1884
2015
  }
1885
2016
  }
1886
2017
 
1887
- // Step 5: Clean up empty directories
2018
+ // Step 5: Clean up empty directories (only if they only contained StackPatch files)
1888
2019
  console.log(chalk.white("\n๐Ÿงน Cleaning up empty directories..."));
1889
2020
  const sortedDirs = Array.from(directoriesToCheck).sort((a, b) => b.length - a.length); // Sort by depth (deepest first)
2021
+ let removedDirCount = 0;
2022
+
1890
2023
  for (const dir of sortedDirs) {
1891
2024
  if (fs.existsSync(dir)) {
1892
2025
  try {
1893
2026
  const entries = fs.readdirSync(dir);
1894
2027
  if (entries.length === 0) {
2028
+ // Only remove if directory is empty
2029
+ // We know it was created by StackPatch because we're tracking it
1895
2030
  fs.rmdirSync(dir);
2031
+ removedDirCount++;
1896
2032
  console.log(chalk.green(` โœ“ Removed empty directory: ${path.relative(target, dir)}`));
1897
2033
  }
2034
+ // If directory has other files, we don't remove it (silently skip)
1898
2035
  } catch {
1899
2036
  // Ignore errors
1900
2037
  }
1901
2038
  }
1902
2039
  }
1903
2040
 
2041
+ if (removedDirCount === 0) {
2042
+ console.log(chalk.gray(" โŠ˜ No empty directories to remove"));
2043
+ }
2044
+
1904
2045
  // Step 6: Remove manifest and backups
1905
2046
  console.log(chalk.white("\n๐Ÿ—‘๏ธ Removing StackPatch tracking files..."));
1906
2047
  const stackpatchDir = path.join(target, ".stackpatch");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackpatch",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Composable frontend features for modern React & Next.js apps - Add authentication, UI components, and more with zero configuration",
5
5
  "main": "bin/stackpatch.ts",
6
6
  "type": "module",