@rollipop/init 1.0.0-beta.1 → 1.0.0-beta.3

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 (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +163 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1 +1,13 @@
1
1
  # @rollipop/init
2
+
3
+ Run from a React Native or Expo project root:
4
+
5
+ ```bash
6
+ yarn dlx @rollipop/init@beta
7
+ yarn install
8
+ ```
9
+
10
+ - Bare projects: logs `bare project detected`, adds the matching `rollipop` version, and configures `react-native.config.js` and `ios/.xcode.env` for Xcode bundle builds. The Xcode setup resolves local or hoisted `node_modules` without running Node before `.xcode.env.local` loads, preserves existing Node settings, and reports custom `CLI_PATH` configurations for manual setup. Projects without `ios` are skipped.
11
+ - Expo projects: logs `expo project detected`, adds matching `rollipop` and `@rollipop/expo` versions, and registers the config plugin in `app.json` or `app.config.json` while preserving existing plugins. Dynamic JS/TS configuration is left unchanged with manual setup instructions.
12
+
13
+ Expo detection uses the `expo` dependency in `package.json`, including prebuilt projects with native directories. See [Quick Start](https://rollipop.dev/docs/get-started/quick-start) or the [Expo guide](https://rollipop.dev/docs/experimental/expo) for setup and limitations.
package/dist/index.js CHANGED
@@ -2,12 +2,93 @@ import pc from "picocolors";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Visitor, parseSync } from "oxc-parser";
5
- //#region package.json
6
- var version = "1.0.0-beta.1";
7
5
  //#endregion
8
6
  //#region src/init.ts
7
+ const { version } = {
8
+ name: "@rollipop/init",
9
+ version: "1.0.0-beta.3",
10
+ homepage: "https://github.com/rollipop-dev/rollipop#readme",
11
+ bugs: { "url": "https://github.com/rollipop-dev/rollipop/issues" },
12
+ license: "MIT",
13
+ author: "leegeunhyeok <dev.ghlee@gmail.com> (https://github.com/leegeunhyeok)",
14
+ repository: {
15
+ "type": "git",
16
+ "url": "git+https://github.com/rollipop-dev/rollipop.git",
17
+ "directory": "packages/core"
18
+ },
19
+ bin: "./bin/index.js",
20
+ files: ["bin", "dist"],
21
+ type: "module",
22
+ scripts: {
23
+ "execute": "node --import @oxc-node/core/register src/index.ts",
24
+ "prepack": "yarn build",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vp test --run --passWithNoTests",
27
+ "build": "vp pack"
28
+ },
29
+ dependencies: {
30
+ "oxc-parser": "0.151.0",
31
+ "picocolors": "1.1.1"
32
+ },
33
+ devDependencies: {
34
+ "@oxc-node/core": "^0.1.3",
35
+ "typescript": "catalog:",
36
+ "vite-plus": "catalog:vp"
37
+ },
38
+ installConfig: { "hoistingLimits": "none" }
39
+ };
9
40
  const RN_CONFIG_FILE = "react-native.config.js";
10
41
  const COMMANDS_REQUIRE = "require('rollipop/commands')";
42
+ const XCODE_CLI_PATH = "export CLI_PATH=\"$PROJECT_DIR/../node_modules/rollipop/bin/index.js\"";
43
+ const XCODE_ENV_SETUP = [
44
+ "# Resolve Rollipop without running Node before .xcode.env.local is loaded.",
45
+ "if [ -z \"${CLI_PATH:-}\" ]; then",
46
+ " ROLLIPOP_CLI_ROOT=\"$(cd \"$PROJECT_DIR/..\" && pwd)\"",
47
+ " while [ ! -f \"$ROLLIPOP_CLI_ROOT/node_modules/rollipop/bin/index.js\" ] && [ \"$ROLLIPOP_CLI_ROOT\" != / ]; do",
48
+ " ROLLIPOP_CLI_ROOT=\"${ROLLIPOP_CLI_ROOT%/*}\"",
49
+ " ROLLIPOP_CLI_ROOT=\"${ROLLIPOP_CLI_ROOT:-/}\"",
50
+ " done",
51
+ " export CLI_PATH=\"$ROLLIPOP_CLI_ROOT/node_modules/rollipop/bin/index.js\"",
52
+ " unset ROLLIPOP_CLI_ROOT",
53
+ "fi"
54
+ ].join("\n");
55
+ function detectProject(cwd) {
56
+ const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
57
+ return packageJson.dependencies?.expo || packageJson.devDependencies?.expo ? "expo" : "bare";
58
+ }
59
+ function setupExpoConfig(cwd) {
60
+ const dynamicConfig = [
61
+ "ts",
62
+ "mts",
63
+ "cts",
64
+ "mjs",
65
+ "cjs",
66
+ "js"
67
+ ].map((extension) => `app.config.${extension}`).find((filename) => fs.existsSync(path.join(cwd, filename)));
68
+ if (dynamicConfig) return {
69
+ status: "manual-required",
70
+ filename: dynamicConfig
71
+ };
72
+ const filename = ["app.config.json", "app.json"].find((name) => fs.existsSync(path.join(cwd, name))) ?? "app.json";
73
+ const configPath = path.join(cwd, filename);
74
+ const exists = fs.existsSync(configPath);
75
+ const root = exists ? JSON.parse(fs.readFileSync(configPath, "utf8")) : { expo: {} };
76
+ if (!root || typeof root !== "object" || Array.isArray(root)) throw new Error(`${filename} must contain an Expo configuration object`);
77
+ const config = "expo" in root ? root.expo : root;
78
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`${filename}: 'expo' must be an object`);
79
+ const plugins = config.plugins ?? [];
80
+ if (!Array.isArray(plugins)) throw new Error(`${filename}: 'plugins' must be an array`);
81
+ if (plugins.some((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin) === "@rollipop/expo")) return {
82
+ status: "already-configured",
83
+ filename
84
+ };
85
+ config.plugins = [...plugins, "@rollipop/expo"];
86
+ fs.writeFileSync(configPath, JSON.stringify(root, null, 2) + "\n");
87
+ return {
88
+ status: exists ? "updated" : "created",
89
+ filename
90
+ };
91
+ }
11
92
  function setupReactNativeConfig(cwd) {
12
93
  const configPath = path.join(cwd, RN_CONFIG_FILE);
13
94
  if (!fs.existsSync(configPath)) {
@@ -27,6 +108,27 @@ function setupReactNativeConfig(cwd) {
27
108
  }
28
109
  }
29
110
  }
111
+ function setupXcodeEnv(cwd) {
112
+ const iosDir = path.join(cwd, "ios");
113
+ if (!fs.existsSync(iosDir)) return "skipped";
114
+ const filename = path.join(iosDir, ".xcode.env");
115
+ const exists = fs.existsSync(filename);
116
+ const content = exists ? fs.readFileSync(filename, "utf8") : "";
117
+ const localFilename = `${filename}.local`;
118
+ const local = fs.existsSync(localFilename) ? fs.readFileSync(localFilename, "utf8") : "";
119
+ const hasCliPath = (source) => source.split(/\r?\n/).some((line) => !line.trimStart().startsWith("#") && /\bCLI_PATH\b/.test(line));
120
+ const hasProjectOverride = fs.readdirSync(iosDir).some((entry) => {
121
+ const projectFile = path.join(iosDir, entry, "project.pbxproj");
122
+ return entry.endsWith(".xcodeproj") && fs.existsSync(projectFile) && /\bCLI_PATH\b/.test(fs.readFileSync(projectFile, "utf8"));
123
+ });
124
+ if (hasCliPath(local) || hasProjectOverride) return "manual-required";
125
+ const configured = content.includes(XCODE_ENV_SETUP) || content.split(/\r?\n/).includes(XCODE_CLI_PATH);
126
+ if (hasCliPath(content.replace(XCODE_ENV_SETUP, "").replace(XCODE_CLI_PATH, ""))) return "manual-required";
127
+ if (configured) return "already-configured";
128
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
129
+ fs.writeFileSync(filename, `${content}${separator}${XCODE_ENV_SETUP}\n`);
130
+ return exists ? "updated" : "created";
131
+ }
30
132
  function analyzeConfig(content) {
31
133
  const { program, comments } = parseSync("react-native.config.js", content);
32
134
  let moduleExportsAssignment = null;
@@ -90,11 +192,16 @@ function applyInjection(content, insertOffset, needsComma) {
90
192
  function setupPackage(cwd) {
91
193
  const packageJsonPath = path.join(cwd, "package.json");
92
194
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
195
+ const dependencies = {
196
+ rollipop: version,
197
+ ...detectProject(cwd) === "expo" ? { "@rollipop/expo": version } : {}
198
+ };
93
199
  packageJson.devDependencies = {
94
200
  ...packageJson.devDependencies,
95
- rollipop: version
201
+ ...dependencies
96
202
  };
97
203
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
204
+ return Object.keys(dependencies);
98
205
  }
99
206
  //#endregion
100
207
  //#region src/index.ts
@@ -114,43 +221,84 @@ const logger = {
114
221
  };
115
222
  console.log(`\n${pc.bold(pc.cyan("rollipop"))} ${pc.dim("— Modern build toolkit for React Native")}\n`);
116
223
  const cwd = process.cwd();
224
+ let projectType;
225
+ try {
226
+ projectType = detectProject(cwd);
227
+ } catch (error) {
228
+ logger.error("Project detection failed", error);
229
+ process.exit(1);
230
+ }
231
+ logger.info(`${projectType} project detected`);
117
232
  let failed = false;
118
233
  let hasWarning = false;
234
+ const guideUrl = projectType === "expo" ? "https://rollipop.dev/docs/experimental/expo" : "https://rollipop.dev/docs/get-started/quick-start";
119
235
  try {
120
- switch (setupReactNativeConfig(cwd)) {
236
+ const { status, filename } = projectType === "expo" ? setupExpoConfig(cwd) : {
237
+ status: setupReactNativeConfig(cwd),
238
+ filename: "react-native.config.js"
239
+ };
240
+ switch (status) {
121
241
  case "created":
122
- logger.success(`Created ${pc.bold("react-native.config.js")}`);
242
+ logger.success(`Created ${pc.bold(filename)}`);
123
243
  break;
124
244
  case "updated":
125
- logger.success(`Updated ${pc.bold("react-native.config.js")}`);
245
+ logger.success(`Updated ${pc.bold(filename)}`);
126
246
  break;
127
247
  case "already-configured":
128
- logger.info(`${pc.bold("react-native.config.js")} already configured`);
248
+ logger.info(`${pc.bold(filename)} already configured`);
249
+ break;
250
+ case "manual-required":
251
+ hasWarning = true;
252
+ if (projectType === "expo") {
253
+ logger.warn(`Could not auto-configure dynamic Expo config: ${pc.bold(filename)}`);
254
+ console.log(` Add ${pc.green("'@rollipop/expo'")} to the exported config's ${pc.bold("plugins")} array, keeping existing entries.`);
255
+ } else {
256
+ logger.warn(`Could not auto-configure: ${pc.bold("module.exports")} is not a plain object`);
257
+ console.log(` ${pc.dim("Add the following to your")} ${pc.bold(filename)}${pc.dim(":")}\n ${pc.green("commands: require('rollipop/commands')")}`);
258
+ }
259
+ }
260
+ } catch (error) {
261
+ logger.error(projectType === "expo" ? "Expo setup failed" : "React Native CLI setup failed", error);
262
+ failed ||= true;
263
+ }
264
+ if (projectType === "bare") try {
265
+ const status = setupXcodeEnv(cwd);
266
+ switch (status) {
267
+ case "created":
268
+ case "updated":
269
+ logger.success(`${status === "created" ? "Created" : "Updated"} ${pc.bold("ios/.xcode.env")}`);
270
+ break;
271
+ case "already-configured":
272
+ logger.info(`${pc.bold("ios/.xcode.env")} already configured`);
273
+ break;
274
+ case "skipped":
275
+ logger.info("No ios directory; skipped Xcode bundle setup");
129
276
  break;
130
277
  case "manual-required":
131
278
  hasWarning = true;
132
- logger.warn(`Could not auto-configure: ${pc.bold("module.exports")} is not a plain object`);
133
- console.log(` ${pc.dim("Add the following to your")} ${pc.bold("react-native.config.js")}${pc.dim(":")}\n ${pc.green("commands: require('rollipop/commands'")})`);
279
+ logger.warn("Existing CLI_PATH configuration requires manual Xcode bundle setup");
280
+ console.log(` Point ${pc.bold("CLI_PATH")} to ${pc.green("rollipop/bin/index.js")} in your Xcode environment or bundle phase, keeping your Node setup.`);
134
281
  }
135
282
  } catch (error) {
136
- logger.error("React Native CLI setup failed", error);
283
+ logger.error("Xcode bundle setup failed", error);
137
284
  failed ||= true;
138
285
  }
139
286
  if (hasWarning || failed) console.log();
140
287
  try {
141
- setupPackage(cwd);
142
- logger.success(`Added ${pc.bold("rollipop")} to ${pc.bold("devDependencies")}`);
288
+ const dependencies = setupPackage(cwd);
289
+ logger.success(`Added ${dependencies.map((dependency) => pc.bold(dependency)).join(", ")} to ${pc.bold("devDependencies")}`);
143
290
  } catch (error) {
144
291
  logger.error("Package setup failed", error);
145
292
  failed ||= true;
146
293
  }
147
294
  if (failed) {
148
- console.log("\n" + pc.red("Setup failed.") + ` Follow the manual guide: ${pc.underline(pc.cyan("https://rollipop.dev/docs/get-started/quick-start"))}`);
295
+ console.log("\n" + pc.red("Setup failed.") + ` Follow the manual guide: ${pc.underline(pc.cyan(guideUrl))}`);
149
296
  process.exit(1);
150
297
  }
151
- logger.success(pc.bold("Setup completed!"));
298
+ if (hasWarning) logger.warn("Manual configuration is required to finish setup.");
299
+ else logger.success(pc.bold("Setup completed!"));
152
300
  console.log();
153
301
  console.log(` ${pc.dim("Install dependencies manually to finish setup.")}`);
154
- console.log(` ${pc.dim("See")} ${pc.underline(pc.cyan("https://rollipop.dev"))} ${pc.dim("for configuration details.")}`);
302
+ console.log(` ${pc.dim("See")} ${pc.underline(pc.cyan(guideUrl))} ${pc.dim("for configuration details.")}`);
155
303
  //#endregion
156
304
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rollipop/init",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "homepage": "https://github.com/rollipop-dev/rollipop#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/rollipop-dev/rollipop/issues"
@@ -32,7 +32,7 @@
32
32
  "devDependencies": {
33
33
  "@oxc-node/core": "^0.1.3",
34
34
  "typescript": "7.0.2",
35
- "vite-plus": "0.3.3"
35
+ "vite-plus": "1.3.0"
36
36
  },
37
37
  "installConfig": {
38
38
  "hoistingLimits": "none"