@rollipop/init 0.1.0-alpha.14 → 0.1.0-alpha.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @rollipop/init
2
2
 
3
+ ## 0.1.0-alpha.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 922142f: update `commands` instead of patching script
8
+
3
9
  ## 0.1.0-alpha.14
4
10
 
5
11
  ## 0.1.0-alpha.13
package/dist/index.js CHANGED
@@ -1,71 +1,86 @@
1
+ import pc from "picocolors";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
- import glob from "fast-glob";
4
- import pc from "picocolors";
5
- import whichPm from "which-pm";
6
- import xcode from "xcode";
4
+ import { Visitor, parseSync } from "oxc-parser";
7
5
 
8
- //#region src/index.ts
9
- const TARGET_BUILD_PHASE_NAME = "Bundle React Native code and images";
10
- const EXPORT_CLI_PATH = "export CLI_PATH=\"$PROJECT_DIR/node_modules/rollipop/bin/index.js\"";
11
- const logger = {
12
- success(message) {
13
- console.log(pc.green("✓"), message);
14
- },
15
- error(message, reason) {
16
- console.error(pc.red("✗"), `${message}: ${reason instanceof Error ? reason.message : String(reason)}`);
6
+ //#region src/init.ts
7
+ const RN_CONFIG_FILE = "react-native.config.js";
8
+ const COMMANDS_REQUIRE = "require('rollipop/commands')";
9
+ function setupReactNativeConfig(cwd) {
10
+ const configPath = path.join(cwd, RN_CONFIG_FILE);
11
+ if (!fs.existsSync(configPath)) {
12
+ fs.writeFileSync(configPath, `module.exports = {\n commands: ${COMMANDS_REQUIRE},\n};\n`);
13
+ return "created";
14
+ }
15
+ const content = fs.readFileSync(configPath, "utf8");
16
+ const result = analyzeConfig(content);
17
+ switch (result.status) {
18
+ case "already-configured": return "already-configured";
19
+ case "has-other-commands": throw new Error(`'commands' property already exists with a different value`);
20
+ case "not-object-export": return "manual-required";
21
+ case "injectable": {
22
+ const updated = applyInjection(content, result.insertOffset, result.needsComma);
23
+ fs.writeFileSync(configPath, updated);
24
+ return "updated";
25
+ }
17
26
  }
18
- };
19
- async function findAppBuildGradle(cwd) {
20
- const files = await glob("**/android/app/build.gradle", { cwd });
21
- if (files.length === 0) throw new Error("No Android build.gradle found");
22
- if (files.length > 1) throw new Error("Multiple Android build.gradle found:\n" + files.join("\n"));
23
- return path.join(cwd, files[0]);
24
27
  }
25
- async function updateGradleCLIPath(appBuildGradlePath) {
26
- const appBuildGradleLines = fs.readFileSync(appBuildGradlePath, "utf8").split("\n");
27
- if (appBuildGradleLines.find((line) => line.trim().startsWith("cliFile ="))) throw new Error(`'cliFile' already configured`);
28
- const index = appBuildGradleLines.findIndex((line) => line.trim().startsWith("react {"));
29
- if (index === -1) throw new Error(`'react' configuration block not found in build.gradle`);
30
- fs.writeFileSync(appBuildGradlePath, [
31
- ...appBuildGradleLines.slice(0, index + 1),
32
- " cliFile = file(\"../../node_modules/rollipop/bin/index.js\")",
33
- ...appBuildGradleLines.slice(index + 1)
34
- ].join("\n"));
28
+ function analyzeConfig(content) {
29
+ const { program } = parseSync("react-native.config.js", content);
30
+ let moduleExportsAssignment = null;
31
+ new Visitor({ AssignmentExpression(node) {
32
+ if (isModuleExports(node)) moduleExportsAssignment = node;
33
+ } }).visit(program);
34
+ if (!moduleExportsAssignment) return { status: "not-object-export" };
35
+ const assignment = moduleExportsAssignment;
36
+ if (assignment.right.type !== "ObjectExpression") return { status: "not-object-export" };
37
+ const objectExpr = assignment.right;
38
+ const commandsProp = findCommandsProperty(objectExpr);
39
+ if (commandsProp) {
40
+ if (isRollipopCommandsRequire(commandsProp, content)) return { status: "already-configured" };
41
+ return { status: "has-other-commands" };
42
+ }
43
+ const properties = objectExpr.properties;
44
+ const lastProp = properties[properties.length - 1];
45
+ const closingBrace = objectExpr.end - 1;
46
+ if (lastProp) {
47
+ const commaIndex = content.substring(lastProp.end, closingBrace).indexOf(",");
48
+ if (commaIndex !== -1) return {
49
+ status: "injectable",
50
+ insertOffset: lastProp.end + commaIndex + 1,
51
+ needsComma: false
52
+ };
53
+ return {
54
+ status: "injectable",
55
+ insertOffset: lastProp.end,
56
+ needsComma: true
57
+ };
58
+ }
59
+ return {
60
+ status: "injectable",
61
+ insertOffset: closingBrace,
62
+ needsComma: false
63
+ };
35
64
  }
36
- async function findXCodeProject(cwd) {
37
- const basePath = path.join(cwd, "ios");
38
- const files = await glob("**/*.xcodeproj/project.pbxproj", {
39
- cwd: basePath,
40
- ignore: ["Pods/**"]
41
- });
42
- if (files.length === 0) throw new Error("No Xcode project found");
43
- if (files.length > 1) throw new Error("Multiple Xcode projects found:\n" + files.join("\n"));
44
- return path.join(basePath, files[0]);
65
+ function isModuleExports(node) {
66
+ const left = node.left;
67
+ return left.type === "MemberExpression" && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports";
45
68
  }
46
- function updateXCodeCLIPath(xcodeProjectPath) {
47
- const project = xcode.project(xcodeProjectPath).parseSync();
48
- const buildPhase = Object.entries(project.hash?.project?.objects?.PBXShellScriptBuildPhase ?? {}).find(([_key, phase]) => phase.name.includes(TARGET_BUILD_PHASE_NAME));
49
- if (buildPhase == null) throw new Error(`'${TARGET_BUILD_PHASE_NAME}' build phase not found`);
50
- const originShellScript = JSON.parse(buildPhase[1].shellScript);
51
- if (originShellScript.includes("CLI_PATH=")) throw new Error(`'CLI_PATH' environment variable already configured`);
52
- const originShellScriptLines = originShellScript.split("\n");
53
- const index = originShellScriptLines.findIndex((line) => line.trim().startsWith("set -"));
54
- let newShellScript = "";
55
- if (index === -1) newShellScript = `${EXPORT_CLI_PATH}\n${originShellScript}`;
56
- else newShellScript = [
57
- ...originShellScriptLines.slice(0, index + 1),
58
- EXPORT_CLI_PATH,
59
- ...originShellScriptLines.slice(index + 1)
60
- ].join("\n");
61
- buildPhase[1].shellScript = JSON.stringify(newShellScript);
62
- fs.writeFileSync(project.filepath, project.writeSync());
69
+ function findCommandsProperty(obj) {
70
+ for (const prop of obj.properties) {
71
+ if (prop.type !== "Property") continue;
72
+ const p = prop;
73
+ if (p.key.type === "Identifier" && p.key.name === "commands" || p.key.type === "Literal" && p.key.value === "commands") return p;
74
+ }
75
+ return null;
63
76
  }
64
- async function setupAndroid(cwd) {
65
- await updateGradleCLIPath(await findAppBuildGradle(cwd));
77
+ function isRollipopCommandsRequire(prop, content) {
78
+ return content.substring(prop.value.start, prop.value.end).includes("rollipop/commands");
66
79
  }
67
- async function setupIos(cwd) {
68
- updateXCodeCLIPath(await findXCodeProject(cwd));
80
+ function applyInjection(content, insertOffset, needsComma) {
81
+ const before = content.substring(0, insertOffset);
82
+ const after = content.substring(insertOffset);
83
+ return `${before}${needsComma ? "," : ""}\n commands: ${COMMANDS_REQUIRE},${after}`;
69
84
  }
70
85
  function setupPackage(cwd) {
71
86
  const packageJsonPath = path.join(cwd, "package.json");
@@ -74,47 +89,66 @@ function setupPackage(cwd) {
74
89
  ...packageJson.devDependencies,
75
90
  rollipop: "latest"
76
91
  };
77
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
92
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
78
93
  }
94
+
95
+ //#endregion
96
+ //#region src/index.ts
97
+ const logger = {
98
+ success(message) {
99
+ console.log(pc.green("✓"), message);
100
+ },
101
+ info(message) {
102
+ console.log(pc.cyan("ℹ"), message);
103
+ },
104
+ warn(message) {
105
+ console.log(pc.yellow("⚠"), message);
106
+ },
107
+ error(message, reason) {
108
+ console.error(pc.red("✗"), `${message}: ${reason instanceof Error ? reason.message : String(reason)}`);
109
+ }
110
+ };
111
+ console.log(`\n${pc.bold(pc.cyan("rollipop"))} ${pc.dim("— Modern build toolkit for React Native")}\n`);
79
112
  const cwd = process.cwd();
80
113
  let failed = false;
114
+ let hasWarning = false;
81
115
  try {
82
- await setupAndroid(cwd);
83
- logger.success("Android setup completed");
84
- } catch (error) {
85
- logger.error("Android setup failed", error);
86
- failed ||= true;
87
- }
88
- try {
89
- await setupIos(cwd);
90
- logger.success("iOS setup completed");
116
+ switch (setupReactNativeConfig(cwd)) {
117
+ case "created":
118
+ logger.success(`Created ${pc.bold("react-native.config.js")}`);
119
+ break;
120
+ case "updated":
121
+ logger.success(`Updated ${pc.bold("react-native.config.js")}`);
122
+ break;
123
+ case "already-configured":
124
+ logger.info(`${pc.bold("react-native.config.js")} already configured`);
125
+ break;
126
+ case "manual-required":
127
+ hasWarning = true;
128
+ logger.warn(`Could not auto-configure: ${pc.bold("module.exports")} is not a plain object`);
129
+ console.log(` ${pc.dim("Add the following to your")} ${pc.bold("react-native.config.js")}${pc.dim(":")}\n ${pc.green("commands: require('rollipop/commands'")})`);
130
+ break;
131
+ }
91
132
  } catch (error) {
92
- logger.error("iOS setup failed", error);
133
+ logger.error("React Native CLI setup failed", error);
93
134
  failed ||= true;
94
135
  }
95
- let packageManager = null;
136
+ if (hasWarning || failed) console.log();
96
137
  try {
97
- packageManager = (await whichPm(cwd))?.name ?? null;
98
138
  setupPackage(cwd);
99
- logger.success("Package setup completed");
139
+ logger.success(`Added ${pc.bold("rollipop")} to ${pc.bold("devDependencies")}`);
100
140
  } catch (error) {
101
141
  logger.error("Package setup failed", error);
102
142
  failed ||= true;
103
143
  }
104
144
  if (failed) {
105
- console.warn(`
106
- Failed to setup project automatically
107
-
108
- Please follow the manual setup guide: ${pc.underline("https://rollipop.dev/docs/get-started/quick-start")}`);
145
+ console.log("\n" + pc.red("Setup failed.") + ` Follow the manual guide: ${pc.underline(pc.cyan("https://rollipop.dev/docs/get-started/quick-start"))}`);
109
146
  process.exit(1);
110
- } else {
111
- logger.success("Project setup completed");
112
- if (packageManager == null) console.log("No package manager found, please install dependencies manually");
113
- else {
114
- const command = pc.bold(`${packageManager} install`);
115
- console.log(`Run ${command} to install dependencies`);
116
- }
117
147
  }
148
+ logger.success(pc.bold("Setup completed!"));
149
+ console.log();
150
+ console.log(` ${pc.dim("Install dependencies manually to finish setup.")}`);
151
+ console.log(` ${pc.dim("See")} ${pc.underline(pc.cyan("https://rollipop.dev"))} ${pc.dim("for configuration details.")}`);
118
152
 
119
153
  //#endregion
120
154
  export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rollipop/init",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.15",
4
4
  "homepage": "https://github.com/leegeunhyeok/rollipop#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/leegeunhyeok/rollipop/issues"
@@ -26,10 +26,8 @@
26
26
  "build": "tsdown"
27
27
  },
28
28
  "dependencies": {
29
- "fast-glob": "^3.3.3",
30
- "picocolors": "^1.1.1",
31
- "which-pm": "^3.0.1",
32
- "xcode": "^3.0.1"
29
+ "oxc-parser": "^0.121.0",
30
+ "picocolors": "^1.1.1"
33
31
  },
34
32
  "devDependencies": {
35
33
  "@oxc-node/core": "^0.0.35",