Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-cases-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-rock': minor
---

feat: add remote cache provider setup
81 changes: 78 additions & 3 deletions packages/create-app/src/lib/__tests__/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ test('should format config without plugins', () => {
ios: platformIOS(),
android: platformAndroid(),
},
remoteCacheProvider: null,
};
"
`);
Expand All @@ -33,7 +32,7 @@ test('should format config with plugins', () => {
},
];

expect(formatConfig([PLATFORMS[0]], plugins, BUNDLERS[1], 'github-actions'))
expect(formatConfig([PLATFORMS[0]], plugins, BUNDLERS[1], null))
.toMatchInlineSnapshot(`
"import { platformIOS } from '@rock-js/platform-ios';
import { pluginTest } from '@rock-js/plugin-test';
Expand All @@ -47,7 +46,83 @@ test('should format config with plugins', () => {
platforms: {
ios: platformIOS(),
},
remoteCacheProvider: 'github-actions',
};
"
`);
});

test(`should format config with the 'github-actions' provider`, () => {
expect(
formatConfig([PLATFORMS[0]], null, BUNDLERS[1], {
name: 'github-actions',
args: { owner: 'custom-owner', repo: 'repo-name', token: 'GITHUB_TOKEN' },
}),
).toMatchInlineSnapshot(`
"import { platformIOS } from '@rock-js/platform-ios';
import { pluginRepack } from '@rock-js/plugin-repack';
import { providerGithubActions } from '@rock-js/provider-github-actions';

export default {
bundler: pluginRepack(),
platforms: {
ios: platformIOS(),
},
remoteCacheProvider: providerGithubActions({
owner: 'custom-owner',
repo: 'repo-name',
token: process.env['GITHUB_TOKEN'],
}),
};
"
`);
});

test(`should format config with the 's3' provider`, () => {
expect(
formatConfig([PLATFORMS[0]], null, BUNDLERS[1], {
name: 's3',
args: { bucket: 'custom-bucket', region: 'us-east-1' },
}),
).toMatchInlineSnapshot(`
"import { platformIOS } from '@rock-js/platform-ios';
import { pluginRepack } from '@rock-js/plugin-repack';
import { providerS3 } from '@rock-js/provider-s3';

export default {
bundler: pluginRepack(),
platforms: {
ios: platformIOS(),
},
remoteCacheProvider: providerS3({
bucket: 'custom-bucket',
region: 'us-east-1',
}),
};
"
`);
});

test(`should format config with the 's3' provider using a custom endpoint`, () => {
expect(
formatConfig([PLATFORMS[0]], null, BUNDLERS[1], {
name: 's3',
args: { bucket: 'custom-bucket', region: 'us-east-1', endpoint: 'https://custom-endpoint.com' },
}),
).toMatchInlineSnapshot(`
"import { platformIOS } from '@rock-js/platform-ios';
import { pluginRepack } from '@rock-js/plugin-repack';
import { providerS3 } from '@rock-js/provider-s3';

export default {
bundler: pluginRepack(),
platforms: {
ios: platformIOS(),
},
remoteCacheProvider: providerS3({
bucket: 'custom-bucket',
region: 'us-east-1',
endpoint: 'https://custom-endpoint.com',
}),
};
"
`);
Expand Down
63 changes: 46 additions & 17 deletions packages/create-app/src/lib/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
BUNDLERS,
PLATFORMS,
PLUGINS,
remoteCacheProviderToConfigTemplate,
remoteCacheProviderToImportTemplate,
resolveTemplate,
TEMPLATES,
} from './templates.js';
Expand Down Expand Up @@ -46,6 +48,7 @@ import {
promptPlugins,
promptProjectName,
promptRemoteCacheProvider,
promptRemoteCacheProviderArgs,
promptTemplate,
} from './utils/prompts.js';
import {
Expand Down Expand Up @@ -146,6 +149,10 @@ export async function run() {
? null
: await promptRemoteCacheProvider();

const remoteCacheProviderArgs = remoteCacheProvider
? await promptRemoteCacheProviderArgs(remoteCacheProvider)
: null;

const shouldInstallDependencies =
options.install || isInteractive()
? await promptInstallDependencies()
Expand All @@ -172,7 +179,12 @@ export async function run() {
platforms,
plugins,
bundler,
remoteCacheProvider,
remoteCacheProvider && remoteCacheProviderArgs
? {
name: remoteCacheProvider,
args: remoteCacheProviderArgs,
}
: null,
);
loader.stop('Applied template, platforms and plugins.');

Expand Down Expand Up @@ -296,7 +308,10 @@ function createConfig(
platforms: TemplateInfo[],
plugins: TemplateInfo[] | null,
bundler: TemplateInfo,
remoteCacheProvider: SupportedRemoteCacheProviders | null,
remoteCacheProvider: {
name: SupportedRemoteCacheProviders;
args: Record<string, unknown>;
} | null,
) {
const rockConfig = path.join(absoluteTargetDir, 'rock.config.mjs');
fs.writeFileSync(
Expand All @@ -309,40 +324,54 @@ export function formatConfig(
platforms: TemplateInfo[],
plugins: TemplateInfo[] | null,
bundler: TemplateInfo,
remoteCacheProvider: SupportedRemoteCacheProviders | null,
remoteCacheProvider: {
name: SupportedRemoteCacheProviders;
args: Record<string, unknown>;
} | null,
) {
const platformsWithImports = platforms.filter(
(template) => template.importName,
);
const pluginsWithImports = plugins
? plugins.filter((template) => template.importName)
: null;
return `${[...platformsWithImports, ...(pluginsWithImports ?? []), bundler]
.map(

return `${[
...[...platformsWithImports, ...(pluginsWithImports ?? []), bundler].map(
(template) =>
`import { ${template.importName} } from '${template.packageName}';`,
)
),
remoteCacheProvider?.name
? remoteCacheProviderToImportTemplate(remoteCacheProvider.name)
: '',
]
.filter(Boolean)
.join('\n')}

export default {${
export default {
${[
pluginsWithImports && pluginsWithImports.length > 0
? `
plugins: [
? `plugins: [
${pluginsWithImports
.map((template) => `${template.importName}(),`)
.join('\n ')}
],`
: ''
}
bundler: ${bundler.importName}(),
platforms: {
: '',
`bundler: ${bundler.importName}(),`,
`platforms: {
${platformsWithImports
.map((template) => `${template.name}: ${template.importName}(),`)
.join('\n ')}
},
remoteCacheProvider: ${
remoteCacheProvider === null ? null : `'${remoteCacheProvider}'`
},
},`,
remoteCacheProvider?.name
? remoteCacheProviderToConfigTemplate(
remoteCacheProvider.name,
remoteCacheProvider.args,
)
: '',
]
.filter(Boolean)
.join('\n ')}
};
`;
}
58 changes: 57 additions & 1 deletion packages/create-app/src/lib/templates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as path from 'node:path';
import { resolveAbsolutePath } from '@rock-js/tools';
import {
resolveAbsolutePath,
type SupportedRemoteCacheProviders,
} from '@rock-js/tools';

export type TemplateInfo = NpmTemplateInfo | LocalTemplateInfo;

Expand Down Expand Up @@ -93,6 +96,59 @@
},
];

export function remoteCacheProviderToImportTemplate(
provider: SupportedRemoteCacheProviders,
) {
switch (provider) {
case 'github-actions':
return `import { providerGithubActions } from '@rock-js/provider-github-actions';`;
case 's3':
return `import { providerS3 } from '@rock-js/provider-s3';`;
}
}

export function remoteCacheProviderToConfigTemplate(
provider: SupportedRemoteCacheProviders,
args: any,

Check warning on line 112 in packages/create-app/src/lib/templates.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
) {
switch (provider) {
case 'github-actions':
return template([
'remoteCacheProvider: providerGithubActions({',
` owner: '${args.owner}',`,
` repo: '${args.repo}',`,
` token: process.env['${args.token}'],`,
'}),',
]);
case 's3':
return template([
'remoteCacheProvider: providerS3({',
` bucket: '${args.bucket}',`,
` region: '${args.region}',`,
[` endpoint: '${args.endpoint}',`, args.endpoint],
'}),',
]);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Cache Config Template Errors

The remoteCacheProviderToConfigTemplate function has two issues. It may return undefined for unhandled provider values. Additionally, user input for arguments like owner or bucket is directly interpolated into the generated config, which can lead to invalid JavaScript syntax if inputs contain single quotes.

Fix in Cursor Fix in Web


export function template(lines: Array<string | [string, boolean]>) {
return lines
.filter((line) => {
// If it's a [content, condition] pair, check the condition
if (Array.isArray(line)) {
return line[1] != null;
}
// If it's just a string, always include it
return true;
})
.map((line) => {
// Extract content from [content, condition] pair or use string directly
const content = Array.isArray(line) ? line[0] : line;
return content;
})
.join('\n ');
}

export function resolveTemplate(
templates: TemplateInfo[],
name: string,
Expand Down
62 changes: 53 additions & 9 deletions packages/create-app/src/lib/utils/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
note,
outro,
promptConfirm,
promptGroup,
promptMultiselect,
promptSelect,
promptText,
Expand All @@ -23,9 +24,9 @@ export function printHelpMessage(
) {
console.log(`
Usage: create-rock [options]

Options:

-h, --help Display help for command
-v, --version Output the version number
-d, --dir Create project in specified directory
Expand All @@ -36,7 +37,7 @@ export function printHelpMessage(
--remote-cache-provider Specify remote cache provider
--override Override files in target directory
--install Install Node.js dependencies

Available templates:
${templates.map((t) => t.name).join(', ')}

Expand Down Expand Up @@ -159,19 +160,62 @@ export function promptBundlers(
});
}

export function promptRemoteCacheProvider(): Promise<SupportedRemoteCacheProviders | null> {
return promptSelect({
message: 'Which remote cache provider do you want to use?',
export function promptRemoteCacheProvider() {
return promptSelect<SupportedRemoteCacheProviders | null>({
message: 'What do you want to use as cache for your remote builds?',
initialValue: 'github-actions',
options: [
{
value: 'github-actions',
label: 'GitHub Actions',
hint: 'Enable builds on your CI',
hint: 'The easiest way to start if you store your code on GitHub',
},
{
value: 's3',
label: 'S3',
hint: 'Work with any S3-compatible storage, including AWS S3 and Cloudflare R2',
},
{
value: null,
label: 'None',
hint: `Local cache only which isn't shared across team members or CI/CD environments`,
},
{ value: null, label: 'None', hint: 'Local builds only' },
],
}) as Promise<SupportedRemoteCacheProviders | null>;
});
}

export function promptRemoteCacheProviderArgs(
provider: SupportedRemoteCacheProviders,
) {
switch (provider) {
case 'github-actions':
return promptGroup({
owner: () => promptText({ message: 'GitHub repository owner' }),
repo: () => promptText({ message: 'GitHub repository name' }),
token: () =>
promptText({
message: 'GitHub Personal Access Token (PAT)',
placeholder: 'GITHUB_TOKEN',
defaultValue: 'GITHUB_TOKEN',
}),
});
case 's3':
return promptGroup({
bucket: () =>
promptText({ message: 'Bucket name', placeholder: 'bucket-name' }),
region: () =>
promptText({
message: 'Region',
placeholder: 'us-west-1',
}),
endpoint: () =>
promptText({
message:
'Endpoint (Only necessary for self-hosted S3 or Cloudflare R2)',
placeholder: 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
}),
});
}
}

export function confirmOverrideFiles(targetDir: string) {
Expand Down
Loading
Loading