Skip to content

Commit

Permalink
update build, deploy, and start tasks to work with SvelteKit (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanatkn authored Apr 7, 2021
1 parent 69467f8 commit f774110
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 101 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ It includes:
## docs

- [unbundled development](/src/docs/unbundled.md) for web frontends, servers, and libraries
- [deploy](/src/docs/deploy.md) to GitHub pages
- [`task`](/src/task) runner
- [dev server](/src/server)
- [`gen`](/src/gen) code generation
Expand Down Expand Up @@ -148,8 +149,13 @@ npm run bootstrap # build and link `gro` - needed only once
gro test # make sure everything looks good - same as `npm test`

# development
gro dev # start dev server in watch mode
gro project/dist # update the `gro` CLI
gro dev # start dev server in watch mode; it's designed as a long-running process
gro build # update the `gro` CLI locally (see comment directly below: slow because bug)
# gro project/dist # TODO this should be the command but it's currently bugged

# use your development version of `gro` locally in another project
cd ../otherproject
npm link ../gro

# release
gro build # build for release and update the `gro` CLI
Expand Down
8 changes: 7 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

## 0.15.0

- **break**: make `src/build.task.ts`, `src/deploy.task.ts`,
and `src/start.task.ts` work with SvelteKit
([#157](https://github.com/feltcoop/gro/pull/157))
- add flag `gro deploy --clean` to reset deployment state
- add flag `--branch` to both tasks, default to `main`
- default to the `deploy` branch instead of `gh-pages`
- **break**: rename `toEnvString` and `toEnvNumber` from `stringFromEnv` and `numberFromEnv`
([#154](https://github.com/feltcoop/gro/pull/154))
([#158](https://github.com/feltcoop/gro/pull/158))

## 0.14.0

Expand Down
67 changes: 47 additions & 20 deletions src/build.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import type {Task} from './task/task.js';
import {createBuild} from './project/build.js';
import type {MapInputOptions, MapOutputOptions, MapWatchOptions} from './project/build.js';
import {getDefaultEsbuildOptions} from './build/esbuildBuildHelpers.js';
import {DIST_DIR, isThisProjectGro, sourceIdToBasePath, toBuildExtension} from './paths.js';
import {
DIST_DIR,
isThisProjectGro,
sourceIdToBasePath,
SVELTE_KIT_BUILD_PATH,
toBuildExtension,
} from './paths.js';
import {Timings} from './utils/time.js';
import {loadGroConfig} from './config/config.js';
import type {GroConfig} from './config/config.js';
import {configureLogLevel} from './utils/log.js';
import {buildSourceDirectory} from './build/buildSourceDirectory.js';
import type {SpawnedProcess} from './utils/process.js';
import {SpawnedProcess, spawnProcess} from './utils/process.js';
import type {TaskEvents as ServerTaskEvents} from './server.task.js';
import {hasApiServerConfig} from './config/defaultBuildConfig.js';
import {hasApiServerConfig, hasSvelteKitFrontend} from './config/defaultBuildConfig.js';
import {printTiming} from './utils/print.js';
import {resolveInputFiles} from './build/utils.js';
import {green} from './utils/terminal.js';
import {toCommonBaseDir} from './utils/path.js';
import {clean} from './fs/clean.js';
import {move} from './fs/node.js';
import {printBuildConfigLabel} from './config/buildConfig.js';

export interface TaskArgs {
mapInputOptions?: MapInputOptions;
Expand Down Expand Up @@ -55,14 +63,29 @@ export const task: Task<TaskArgs, TaskEvents> = {

const esbuildOptions = getDefaultEsbuildOptions(config.target, config.sourcemap, dev);

const timingToClean = timings.start('clean');
await clean({dist: true}, log);
timingToClean();

// If this is a SvelteKit frontend, for now, just build it and exit immediately.
// TODO support merging SvelteKit and Gro builds (and then delete `felt-server`'s build task)
if ((await hasSvelteKitFrontend()) && !isThisProjectGro) {
const timingToBuildSvelteKit = timings.start('SvelteKit build');
await spawnProcess('npx', ['svelte-kit', 'build']);
await move(SVELTE_KIT_BUILD_PATH, DIST_DIR);
timingToBuildSvelteKit();
}

// TODO think this through
// This is like a "prebuild" phase.
// Build everything with esbuild and Gro's `Filer` first,
// so we have the production server available to run while SvelteKit is building.
// See the other reference to `isThisProjectGro` for comments about its weirdness.
let spawnedApiServer: SpawnedProcess | null = null;
if (!isThisProjectGro) {
const timingToPrebuild = timings.start('prebuild');
await buildSourceDirectory(config, dev, log);
timingToPrebuild();
events.emit('build.prebuild');

// now that the prebuild is ready, we can start the API server, if it exists
Expand All @@ -85,32 +108,34 @@ export const task: Task<TaskArgs, TaskEvents> = {
// and therefore belong in the default Rollup build.
// If more customization is needed, users should implement their own `src/build.task.ts`,
// which can be bootstrapped by copy/pasting this one. (and updating the imports)
const timingToBuild = timings.start('build');
await Promise.all(
buildConfigsToBuild.map(async (buildConfig) => {
const inputFiles = await resolveInputFiles(buildConfig);
if (!inputFiles.length) {
log.trace('no input files in', printBuildConfigLabel(buildConfig));
return;
}
// TODO ok wait, does `outputDir` need to be at the output dir path?
const outputDir = `${DIST_DIR}${toBuildExtension(
sourceIdToBasePath(toCommonBaseDir(inputFiles)),
)}`;
// const outputDir = paths.dist;
log.info(`building ${green(buildConfig.name)}`, outputDir, inputFiles);
if (inputFiles.length) {
const build = createBuild({
dev,
sourcemap: config.sourcemap,
inputFiles,
outputDir,
mapInputOptions,
mapOutputOptions,
mapWatchOptions,
esbuildOptions,
});
await build.promise;
} else {
log.warn(`no input files in build "${buildConfig.name}"`);
}
log.info('building', printBuildConfigLabel(buildConfig), outputDir, inputFiles);
const build = createBuild({
dev,
sourcemap: config.sourcemap,
inputFiles,
outputDir,
mapInputOptions,
mapOutputOptions,
mapWatchOptions,
esbuildOptions,
});
await build.promise;
}),
);
timingToBuild();

// done! clean up the API server
if (spawnedApiServer) {
Expand All @@ -119,7 +144,9 @@ export const task: Task<TaskArgs, TaskEvents> = {
args.closeApiServer(spawnedApiServer);
} else {
spawnedApiServer!.child.kill();
const timingToCloseServer = timings.start('close server');
await spawnedApiServer!.closed;
timingToCloseServer();
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/cert.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const task: Task<TaskArgs> = {
const host = args.host || 'localhost';
const certFile = `${host}-cert.pem`;
const keyFile = `${host}-privkey.pem`;
if (await pathExists(certFile)) throw Error(`File ${certFile} already exists. Aborting.`);
if (await pathExists(keyFile)) throw Error(`File ${keyFile} already exists. Aborting.`);
if (await pathExists(certFile)) throw Error(`File ${certFile} already exists. Canceling.`);
if (await pathExists(keyFile)) throw Error(`File ${keyFile} already exists. Canceling.`);
await spawnProcess(
'openssl',
`req -x509 -newkey rsa:2048 -nodes -sha256 -subj /CN=${host} -keyout ${keyFile} -out ${certFile}`.split(
Expand Down
20 changes: 13 additions & 7 deletions src/config/defaultBuildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {toBuildExtension, basePathToSourceId, toBuildOutPath, paths} from '../pa
import {pathExists} from '../fs/node.js';
import {getExtensions} from '../fs/mime.js';

export const GIT_DEPLOY_BRANCH = 'main'; // deploy and publish from this branch

// Gro currently enforces that the primary build config
// for the Node platform has this value as its name.
// This convention speeds up running tasks by standardizing where Gro can look for built files.
Expand Down Expand Up @@ -43,17 +45,21 @@ export const API_SERVER_DEFAULT_PORT_DEV = 3001;
export const toApiServerBuildPath = (dev: boolean, buildDir = paths.build): string =>
toBuildOutPath(dev, API_SERVER_BUILD_CONFIG_NAME, API_SERVER_BUILD_BASE_PATH, buildDir);

export const hasDeprecatedGroFrontend = async (): Promise<boolean> => {
const [hasIndexHtml, hasIndexTs] = await Promise.all([
pathExists('src/index.html'),
pathExists('src/index.ts'),
]);
return hasIndexHtml && hasIndexTs;
};
const SVELTE_KIT_FRONTEND_PATHS = ['src/app.html', 'src/routes'];
export const hasSvelteKitFrontend = async (): Promise<boolean> =>
everyPathExists(SVELTE_KIT_FRONTEND_PATHS);

const DEPRECATED_GRO_FRONTEND_PATHS = ['src/index.html', 'src/index.ts'];
export const hasDeprecatedGroFrontend = async (): Promise<boolean> =>
everyPathExists(DEPRECATED_GRO_FRONTEND_PATHS);

export const toDefaultBrowserBuild = (assetPaths = toDefaultAssetPaths()): PartialBuildConfig => ({
name: 'browser',
platform: 'browser',
input: ['index.ts', createFilter(`**/*.{${assetPaths.join(',')}}`)],
dist: true,
});
const toDefaultAssetPaths = (): string[] => Array.from(getExtensions());

const everyPathExists = async (paths: string[]): Promise<boolean> =>
(await Promise.all(paths.map((path) => pathExists(path)))).every((v) => !!v);
96 changes: 63 additions & 33 deletions src/deploy.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,120 @@ import {join, basename} from 'path';

import type {Task} from './task/task.js';
import {spawnProcess} from './utils/process.js';
import {copy, pathExists} from './fs/node.js';
import {copy} from './fs/node.js';
import {paths} from './paths.js';
import {printError, printPath} from './utils/print.js';
import {magenta, green, rainbow, red} from './utils/terminal.js';
import {GIT_DEPLOY_BRANCH} from './config/defaultBuildConfig.js';

// TODO support other kinds of deployments
// TODO add a flag to delete the existing deployment branch to avoid bloat (and maybe run `git gc --auto`)

export interface TaskArgs {
branch?: string; // optional branch to deploy from; defaults to 'main'
dry?: boolean;
clean?: boolean; // clean the git worktree and Gro cache
}

// TODO customize
const distDir = paths.dist;
const distDirName = basename(distDir);
const deploymentBranch = 'gh-pages';
const deploymentStaticContentDir = join(paths.source, 'project/gh-pages/');
const initialFile = 'package.json';
const deploymentBranch = 'deploy';
const initialFile = 'package.json'; // this is a single file that's copied into the new branch to bootstrap it
const TEMP_PREFIX = '__TEMP__';

// TODO support other kinds of deployments
// TODO add a flag to delete the existing deployment branch to avoid bloat (and maybe run `git gc --auto`)

export const task: Task<TaskArgs> = {
description: 'deploy to gh-pages',
description: 'deploy to static hosting',
run: async ({invokeTask, args, log}): Promise<void> => {
const {dry} = args;
const {branch, dry, clean} = args;

const sourceBranch = branch || GIT_DEPLOY_BRANCH;

// Exit early if the git working directory has any unstaged or staged changes.
const gitDiffResult = await spawnProcess('git', ['diff-index', '--quiet', 'HEAD']);
if (!gitDiffResult.ok) {
log.error(red('git working directory is unclean~ please commit or stash to proceed'));
return;
}

// Ensure we're on the right branch.
const gitCheckoutResult = await spawnProcess('git', ['checkout', sourceBranch]);
if (!gitCheckoutResult.ok) {
log.error(red(`failed git checkout with exit code ${gitCheckoutResult.code}`));
return;
}

// TODO filter stdout? `--quiet` didn't work
// Set up the deployment branch if necessary.
// If the `deploymentBranch` already exists, this is a no-op.
log.info(magenta('↓↓↓↓↓↓↓'), green('ignore any errors in here'), magenta('↓↓↓↓↓↓↓'));
await spawnProcess(
`git checkout --orphan ${deploymentBranch} && ` +
// TODO there's definitely a better way to do this
`cp ${initialFile} ${TEMP_PREFIX}${initialFile} && ` +
`git rm -rf . && ` +
`mv ${TEMP_PREFIX}${initialFile} ${initialFile} && ` +
`git add ${initialFile} && ` +
'git commit -m "setup" && git checkout main',
`git commit -m "setup" && git checkout ${sourceBranch}`,
[],
// this uses `shell: true` because the above is unwieldy with standard command construction
{shell: true},
);

// Clean up any existing worktree.
await cleanGitWorktree();
log.info(magenta('↑↑↑↑↑↑↑'), green('ignore any errors in here'), magenta('↑↑↑↑↑↑↑'));

// Get ready to build from scratch.
await invokeTask('clean');

if (clean) {
log.info(rainbow('all clean'));
return;
}

// Set up the deployment worktree in the dist directory.
await spawnProcess('git', ['worktree', 'add', distDirName, deploymentBranch]);

try {
// Run the build.
await invokeTask('build');

// Copy everything from `src/project/gh-pages`. (like `CNAME` for GitHub custom domains)
// If any files conflict, throw an error!
// That should be part of the build process.
if (await pathExists(deploymentStaticContentDir)) {
await copy(deploymentStaticContentDir, distDir);
}
// Update the initial file.
await copy(initialFile, join(distDir, initialFile));
} catch (err) {
log.error('Build failed:', printError(err));
log.error(red('build failed'), 'but', green('no changes were made to git'), printError(err));
if (dry) {
log.info('Dry deploy failed! Files are available in', printPath(distDirName));
log.info(red('dry deploy failed:'), 'files are available in', printPath(distDirName));
} else {
await cleanGitWorktree(true);
}
throw Error(`Deploy aborted due to build failure. See the error above.`);
throw Error(`Deploy safely canceled due to build failure. See the error above.`);
}

// At this point, `dist/` is ready to be committed and deployed!
if (dry) {
log.info('Dry deploy complete! Files are available in', printPath(distDirName));
} else {
await spawnProcess('git', ['add', '.'], {cwd: distDir});
await spawnProcess('git', ['commit', '-m', 'deployment'], {
cwd: distDir,
});
await spawnProcess('git', ['push', 'origin', deploymentBranch], {
cwd: distDir,
});

// Clean up the worktree so it doesn't interfere with development.
// TODO maybe add a flag to preserve these files instead of overloading `dry`?
// or maybe just create a separate `deploy` dir to avoid problems?
await cleanGitWorktree();
log.info(green('dry deploy complete:'), 'files are available in', printPath(distDirName));
return;
}

try {
// TODO wait is this `cwd` correct or vestiges of the old code?
const gitArgs = {cwd: distDir};
await spawnProcess('git', ['add', '.'], gitArgs);
await spawnProcess('git', ['commit', '-m', 'deployment'], gitArgs);
await spawnProcess('git', ['push', 'origin', deploymentBranch], gitArgs);
} catch (err) {
log.error(red('updating git failed:'), printError(err));
throw Error(`Deploy failed in a bad state: built but not pushed. See the error above.`);
}

// Clean up the worktree so it doesn't interfere with development.
// TODO maybe add a flag to preserve these files instead of overloading `dry`?
// or maybe just create a separate `deploy` dir to avoid problems?
await cleanGitWorktree();

log.info(rainbow('deployed'));
},
};

Expand Down
1 change: 1 addition & 0 deletions src/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
> <sub>[gro](/../..) / docs / README.md</sub>
- [config](config.md)
- [deploy](deploy.md)
- [options](options.md)
- [publish](publish.md)
- [tasks](tasks.md)
Expand Down
16 changes: 16 additions & 0 deletions src/docs/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# gro/deploy

For now, Gro's `gro deploy` task supports only static deployments to
[GitHub pages](https://pages.github.com/).
Eventually we want to expand builtin support,
including servers and other static clouds,
but for now you need to implement `src/deploy.task.ts` yourself outside of GitHub pages.

```bash
gro deploy # prepares dist/
gro deploy --branch my-branch # deploy from a branch other than 'main'
gro deploy --dry # prepare dist/ but don't commit or push
gro deploy --clean # if something goes wrong, this resets git and gro state
```

See [`src/deploy.task.ts`](/src/deploy.task.ts) for more.
2 changes: 1 addition & 1 deletion src/docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ What is a `Task`? See [`src/tasks/README.md`](../task).
- [cert](../cert.task.ts) - creates a self-signed cert for https with openssl
- [check](../check.task.ts) - check that everything is ready to commit
- [clean](../clean.task.ts) - remove temporary dev and build files
- [deploy](../deploy.task.ts) - deploy to gh-pages
- [deploy](../deploy.task.ts) - deploy to static hosting
- [dev](../dev.task.ts) - start dev server
- [format](../format.task.ts) - format source files
- [gen](../gen.task.ts) - run code generation scripts
Expand Down
Loading

0 comments on commit f774110

Please sign in to comment.