Skip to content

Commit

Permalink
feat(cli, config-plugins): [2/3] patch based config-plugin (expo#26414)
Browse files Browse the repository at this point in the history
# Why

supersedes expo#26199

# How

this pr changes `@expo/cli` and mainly to add the
`config._internal.templateChecksum`
- calculate md5 template checksum when reading from template tarball's
readable stream
- fill `config._internal.templateChecksum` when prebuild
- export `cloneTemplateAndCopyToProjectAsync` for prebuild-patch
  • Loading branch information
Kudo authored Jan 16, 2024
1 parent da2abae commit 64c7499
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 129 deletions.
1 change: 1 addition & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### 💡 Others

- Remove classic updates SDK version. ([#26061](https://github.com/expo/expo/pull/26061) by [@wschurman](https://github.com/wschurman))
- Added `templateChecksum` for prebuild to check the current template version. ([#26414](https://github.com/expo/expo/pull/26414) by [@kudo](https://github.com/kudo))

## 0.16.8 - 2024-01-15

Expand Down
4 changes: 2 additions & 2 deletions packages/@expo/cli/src/export/exportStaticAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ export function getHtmlFiles({
baseUrl === ''
? 'index'
: baseUrl.endsWith('/')
? baseUrl + 'index'
: baseUrl.slice(0, -1);
? baseUrl + 'index'
: baseUrl.slice(0, -1);
}

// This should never happen, the type of `string | object` originally comes from React Navigation.
Expand Down
8 changes: 8 additions & 0 deletions packages/@expo/cli/src/prebuild/configureProjectAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ export async function configureProjectAsync(
{
platforms,
exp,
templateChecksum,
}: {
platforms: ModPlatform[];
exp?: ExpoConfig;
templateChecksum?: string;
}
): Promise<ExpoConfig> {
let bundleIdentifier: string | undefined;
Expand All @@ -37,6 +39,12 @@ export async function configureProjectAsync(
bundleIdentifier,
});

if (templateChecksum) {
// Prepare template checksum for the patch mods
config._internal = config._internal ?? {};
config._internal.templateChecksum = templateChecksum;
}

// compile all plugins and mods
config = await compileModsAsync(config, {
projectRoot,
Expand Down
3 changes: 2 additions & 1 deletion packages/@expo/cli/src/prebuild/prebuildAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export async function prebuildAsync(
const { exp, pkg } = await ensureConfigAsync(projectRoot, { platforms: options.platforms });

// Create native projects from template.
const { hasNewProjectFiles, needsPodInstall, changedDependencies } =
const { hasNewProjectFiles, needsPodInstall, templateChecksum, changedDependencies } =
await updateFromTemplateAsync(projectRoot, {
exp,
pkg,
Expand Down Expand Up @@ -142,6 +142,7 @@ export async function prebuildAsync(
await profile(configureProjectAsync)(projectRoot, {
platforms: options.platforms,
exp,
templateChecksum,
});
configSyncingStep.succeed('Finished prebuild');
} catch (error) {
Expand Down
119 changes: 57 additions & 62 deletions packages/@expo/cli/src/prebuild/resolveTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExpoConfig } from '@expo/config';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs';
import { Ora } from 'ora';
Expand Down Expand Up @@ -34,12 +35,12 @@ export async function cloneTemplateAsync({
template?: string;
exp: Pick<ExpoConfig, 'name' | 'sdkVersion'>;
ora: Ora;
}) {
}): Promise<string> {
if (template) {
await resolveTemplateArgAsync(templateDirectory, ora, exp.name, template);
return await resolveTemplateArgAsync(templateDirectory, ora, exp.name, template);
} else {
const templatePackageName = await getTemplateNpmPackageName(exp.sdkVersion);
await downloadAndExtractNpmModuleAsync(templatePackageName, {
return await downloadAndExtractNpmModuleAsync(templatePackageName, {
cwd: templateDirectory,
name: exp.name,
});
Expand Down Expand Up @@ -92,14 +93,14 @@ function hasRepo({ username, name, branch, filePath }: RepoInfo) {
async function downloadAndExtractRepoAsync(
root: string,
{ username, name, branch, filePath }: RepoInfo
): Promise<void> {
): Promise<string> {
const projectName = path.basename(root);

const strip = filePath ? filePath.split('/').length + 1 : 1;

const url = `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`;
debug('Downloading tarball from:', url);
await extractNpmTarballFromUrlAsync(url, {
return await extractNpmTarballFromUrlAsync(url, {
cwd: root,
name: projectName,
strip,
Expand All @@ -113,76 +114,70 @@ export async function resolveTemplateArgAsync(
appName: string,
template: string,
templatePath?: string
) {
let repoInfo: RepoInfo | undefined;
): Promise<string> {
assert(template, 'template is required');

if (template) {
// @ts-ignore
let repoUrl: URL | undefined;

try {
// @ts-ignore
repoUrl = new URL(template);
} catch (error: any) {
if (error.code !== 'ERR_INVALID_URL') {
oraInstance.fail(error);
throw error;
}
}
let repoUrl: URL | undefined;

// On Windows, we can actually create a URL from a local path
// Double-check if the created URL is not a path to avoid mixing up URLs and paths
if (process.platform === 'win32' && repoUrl && path.isAbsolute(repoUrl.toString())) {
repoUrl = undefined;
try {
repoUrl = new URL(template);
} catch (error: any) {
if (error.code !== 'ERR_INVALID_URL') {
oraInstance.fail(error);
throw error;
}
}

if (!repoUrl) {
const templatePath = path.resolve(template);
if (!fs.existsSync(templatePath)) {
throw new CommandError(`template file does not exist: ${templatePath}`);
}

await extractLocalNpmTarballAsync(templatePath, { cwd: templateDirectory, name: appName });
return templateDirectory;
}
// On Windows, we can actually create a URL from a local path
// Double-check if the created URL is not a path to avoid mixing up URLs and paths
if (process.platform === 'win32' && repoUrl && path.isAbsolute(repoUrl.toString())) {
repoUrl = undefined;
}

if (repoUrl.origin !== 'https://github.com') {
oraInstance.fail(
`Invalid URL: ${chalk.red(
`"${template}"`
)}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
);
throw new AbortCommandError();
if (!repoUrl) {
const templatePath = path.resolve(template);
if (!fs.existsSync(templatePath)) {
throw new CommandError(`template file does not exist: ${templatePath}`);
}

repoInfo = await getRepoInfo(repoUrl, templatePath);
return await extractLocalNpmTarballAsync(templatePath, {
cwd: templateDirectory,
name: appName,
});
}

if (!repoInfo) {
oraInstance.fail(
`Found invalid GitHub URL: ${chalk.red(`"${template}"`)}. Please fix the URL and try again.`
);
throw new AbortCommandError();
}
if (repoUrl.origin !== 'https://github.com') {
oraInstance.fail(
`Invalid URL: ${chalk.red(
`"${template}"`
)}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
);
throw new AbortCommandError();
}

const found = await hasRepo(repoInfo);
const repoInfo = await getRepoInfo(repoUrl, templatePath);

if (!found) {
oraInstance.fail(
`Could not locate the repository for ${chalk.red(
`"${template}"`
)}. Please check that the repository exists and try again.`
);
throw new AbortCommandError();
}
if (!repoInfo) {
oraInstance.fail(
`Found invalid GitHub URL: ${chalk.red(`"${template}"`)}. Please fix the URL and try again.`
);
throw new AbortCommandError();
}

if (repoInfo) {
oraInstance.text = chalk.bold(
`Downloading files from repo ${chalk.cyan(template)}. This might take a moment.`
);
const found = await hasRepo(repoInfo);

await downloadAndExtractRepoAsync(templateDirectory, repoInfo);
if (!found) {
oraInstance.fail(
`Could not locate the repository for ${chalk.red(
`"${template}"`
)}. Please check that the repository exists and try again.`
);
throw new AbortCommandError();
}

return true;
oraInstance.text = chalk.bold(
`Downloading files from repo ${chalk.cyan(template)}. This might take a moment.`
);

return await downloadAndExtractRepoAsync(templateDirectory, repoInfo);
}
16 changes: 11 additions & 5 deletions packages/@expo/cli/src/prebuild/updateFromTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ export async function updateFromTemplateAsync(
hasNewProjectFiles: boolean;
/** Indicates that the project needs to run `pod install` */
needsPodInstall: boolean;
/** The template checksum used to create the native project. */
templateChecksum: string;
} & DependenciesModificationResults
> {
if (!templateDirectory) {
const temporary = await import('tempy');
templateDirectory = temporary.directory();
}

const copiedPaths = await profile(cloneTemplateAndCopyToProjectAsync)({
const { copiedPaths, templateChecksum } = await profile(cloneTemplateAndCopyToProjectAsync)({
projectRoot,
template,
templateDirectory,
Expand All @@ -70,6 +72,7 @@ export async function updateFromTemplateAsync(
hasNewProjectFiles: !!copiedPaths.length,
// If the iOS folder changes or new packages are added, we should rerun pod install.
needsPodInstall: copiedPaths.includes('ios') || !!depsResults.changedDependencies.length,
templateChecksum,
...depsResults,
};
}
Expand All @@ -79,7 +82,7 @@ export async function updateFromTemplateAsync(
*
* @return `true` if any project files were created.
*/
async function cloneTemplateAndCopyToProjectAsync({
export async function cloneTemplateAndCopyToProjectAsync({
projectRoot,
templateDirectory,
template,
Expand All @@ -91,7 +94,7 @@ async function cloneTemplateAndCopyToProjectAsync({
template?: string;
exp: Pick<ExpoConfig, 'name' | 'sdkVersion'>;
platforms: ModPlatform[];
}): Promise<string[]> {
}): Promise<{ copiedPaths: string[]; templateChecksum: string }> {
const platformDirectories = unknownPlatforms
.map((platform) => `./${platform}`)
.reverse()
Expand All @@ -101,7 +104,7 @@ async function cloneTemplateAndCopyToProjectAsync({
const ora = logNewSection(`Creating native ${pluralized} (${platformDirectories})`);

try {
await cloneTemplateAsync({ templateDirectory, template, exp, ora });
const templateChecksum = await cloneTemplateAsync({ templateDirectory, template, exp, ora });

const platforms = validateTemplatePlatforms({
templateDirectory,
Expand All @@ -115,7 +118,10 @@ async function cloneTemplateAndCopyToProjectAsync({

ora.succeed(createCopyFilesSuccessMessage(platforms, results));

return results.copiedPaths;
return {
copiedPaths: results.copiedPaths,
templateChecksum,
};
} catch (e: any) {
if (!(e instanceof AbortCommandError)) {
Log.error(e.message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const createContext = ({
mainFields: preferNativePlatform
? ['react-native', 'browser', 'main']
: isServer
? ['main', 'module']
: ['browser', 'module', 'main'],
? ['main', 'module']
: ['browser', 'module', 'main'],
nodeModulesPaths: ['node_modules', ...nodeModulesPaths],
originModulePath: origin,
preferNativePlatform,
Expand All @@ -48,8 +48,8 @@ const createContext = ({
unstable_conditionNames: isServer
? ['node', 'require']
: platform === 'web'
? ['require', 'import', 'browser']
: ['require', 'import', 'react-native'],
? ['require', 'import', 'browser']
: ['require', 'import', 'react-native'],
};
};

Expand Down Expand Up @@ -124,8 +124,8 @@ function resolveTo(
return res.type === 'sourceFile'
? res.filePath
: res.type === 'assetFiles'
? res.filePaths[0]
: null;
? res.filePaths[0]
: null;
}

describe(createFastResolver, () => {
Expand Down
Loading

0 comments on commit 64c7499

Please sign in to comment.