diff --git a/README.md b/README.md index c173c4cb67..f20c052fd9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ > **Windows is not yet supported.** WSL works well enough -> _warning!_ You should not use Gro today +> _warning_: You should not use Gro today > unless you're willing to take ownership of the code. > For now, consider Gro's free software > [free as in puppy](https://twitter.com/GalaxyKate/status/1371159136684105728), @@ -40,13 +40,13 @@ It includes: UIs along with Node servers, JS/TS/Svelte libraries, and other things (see the [config docs](/src/docs/config.md), the [SvelteKit integration docs](/src/docs/sveltekit.md), and [the default config](https://github.com/feltcoop/gro/blob/main/src/config/gro.config.default.ts)) + - fully integrated [TypeScript](https://github.com/microsoft/typescript) + and [Svelte](https://github.com/sveltejs/svelte) + using [esbuild](https://github.com/evanw/esbuild) in dev mode for speed - [configurable adapters](/src/docs/adapt.md) featuring e.g. optional production bundling with [Rollup](https://github.com/rollup/rollup) - [configurable plugins](/src/docs/plugin.md) to support SvelteKit, auto-restarting API servers, and other external build processes - - fully integrated [TypeScript](https://github.com/microsoft/typescript) - and [Svelte](https://github.com/sveltejs/svelte) - using [esbuild](https://github.com/evanw/esbuild) in dev mode for speed - [task runner](/src/docs/task.md) that uses the filesystem convention `*.task.ts` - lots of [common default tasks](/src/docs/tasks.md) that projects can easily override and compose - [testing](/src/docs/test.md) with [`uvu`](https://github.com/lukeed/uvu) diff --git a/changelog.md b/changelog.md index a21c501697..b48462a9ce 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,11 @@ # changelog -## 0.45.2 +## 0.46.0 +- **break**: change task `dev` property to `production` + ([#284](https://github.com/feltcoop/gro/pull/284)) +- fix `process.env.NODE_ENV` for production tasks + ([#284](https://github.com/feltcoop/gro/pull/284)) - clean dist for production builds ([#285](https://github.com/feltcoop/gro/pull/285)) diff --git a/src/build.task.ts b/src/build.task.ts index 0bbf93dea4..661f859f16 100644 --- a/src/build.task.ts +++ b/src/build.task.ts @@ -24,7 +24,7 @@ export interface TaskEvents { export const task: Task = { summary: 'build the project', - dev: false, + production: true, run: async (ctx): Promise => { const {fs, dev, log, events, args} = ctx; diff --git a/src/cli/invoke.ts b/src/cli/invoke.ts index c57ce1d61f..3f37e37bea 100644 --- a/src/cli/invoke.ts +++ b/src/cli/invoke.ts @@ -22,7 +22,7 @@ const main = async () => { // install sourcemaps for Gro development if (process.env.NODE_ENV !== 'production') { - const sourcemapSupport = await import('source-map-support'); + const sourcemapSupport = await import('source-map-support'); // is a peer dependency sourcemapSupport.install({ handleUncaughtExceptions: false, }); diff --git a/src/config/gro.config.default.ts b/src/config/gro.config.default.ts index 2313ee9f13..079c199698 100644 --- a/src/config/gro.config.default.ts +++ b/src/config/gro.config.default.ts @@ -15,12 +15,9 @@ This is the default config that's passed to `src/gro.config.ts` if it exists in the current project, and if not, this is the final config. It looks at the project and tries to do the right thing: -- if `src/routes` and `src/app.html`, - assumes a SvelteKit frontend -- if `src/lib/index.ts`, - assumes a Node library -- if `src/lib/server/server.ts`, - assumes a Node API server +- if `src/routes` and `src/app.html`, assumes a SvelteKit frontend +- if `src/lib/index.ts`, assumes a Node library +- if `src/lib/server/server.ts`, assumes a Node API server */ diff --git a/src/deploy.task.ts b/src/deploy.task.ts index 75c4cfe838..727191051b 100644 --- a/src/deploy.task.ts +++ b/src/deploy.task.ts @@ -38,7 +38,7 @@ const EXCLUDED_BRANCHES = ['main', 'master']; export const task: Task = { summary: 'deploy to static hosting', - dev: false, + production: true, run: async ({fs, invokeTask, args, log}): Promise => { const {dirname, branch, dry, clean: cleanAndExit, force} = args; @@ -104,7 +104,7 @@ export const task: Task = { try { // Run the build. - await invokeTask('build', {...args, clean: false}); + await invokeTask('build'); // After the build is ready, set the deployed directory, inferring as needed. if (dirname !== undefined) { diff --git a/src/docs/sveltekit.md b/src/docs/sveltekit.md index ae364f5fc1..3de37fd661 100644 --- a/src/docs/sveltekit.md +++ b/src/docs/sveltekit.md @@ -18,21 +18,11 @@ Gro has two important differences from SvelteKit: - Gro ignores SvelteKit's [library packaging](https://kit.svelte.dev/docs#packaging) capabilities for its own with [`gro publish`](./publish.md) -Beyond this, Gro mostly stays out of SvelteKit's way. +Beyond this, Gro mostly stays out of SvelteKit's way, +and the eventual goal is to defer to SvelteKit as much as possible. You can still use `svelte-kit package` but it doesn't currently integrate with Gro's other systems, checks, and conventions. Gro's supplemental role is still a work in progress -- in the current implementation, user projects manage their own SvelteKit dependencies, and commands like `gro dev` and `gro build` automatically detect SvelteKit projects. - -Gro supports SvelteKit+Vite along with its own non-conflicting system for frontend development, -but Gro's frontend system does not currently support HMR, code splitting, routing, -and many other things provided by SvelteKit+Vite. -It's appropriate only for a small number of specific usecases -and can be wholly replaced with SvelteKit. -SvelteKit should be preferred by users today, -and Gro's frontend conventions are currently undocumented. -If you're curious, examples of Gro's frontend functionality include Gro's -[`src/client`](/src/client) and -[`ryanatkn/mirror-twin-gro`](https://github.com/ryanatkn/mirror-twin-gro). diff --git a/src/docs/task.md b/src/docs/task.md index 6518b60a8a..fd2866d503 100644 --- a/src/docs/task.md +++ b/src/docs/task.md @@ -12,14 +12,14 @@ ## what -A Gro task is just a function with some optional metadata. +A Gro `Task` is just an object with a `run` function and some optional metadata. Gro prefers conventions and code over configuration, and its task runner leverages the filesystem as the API and defers composition to the user in regular TypeScript modules. - Gro automatically discovers [all `*.task.ts` files](../docs/tasks.md) in your source directory, so creating a new task is as simple as creating a new file - - no configuration or scaffolding commands needed! + no configuration needed - task definitions are just objects with an async `run` function and some optional properties, so composing tasks is explicit in your code, just like any other module (but there's also the helper `invokeTask`: see more below) @@ -294,7 +294,7 @@ export const task: Task = { import type {Task} from '@feltcoop/gro'; export const task: Task = { - dev: false, // tell the task runner to set `dev` to false, updating `process.env.NODE_ENV` + production: true, // task runner will spawn a new process if `process.env.NODE_ENV` isn't 'production' run: async ({dev, invokeTask}) => { // `dev` is `false` because it's defined two lines up in the task definition, // unless an ancestor task called `invokeTask` with a `true` value, like this: diff --git a/src/fs/mime.ts b/src/fs/mime.ts index 40e0fe12d1..271c6b616f 100644 --- a/src/fs/mime.ts +++ b/src/fs/mime.ts @@ -69,6 +69,9 @@ export const removeMimeTypeExtension = (extension: FileExtension): boolean => { ['application/schema+json', ['json']], ['application/schema-instance+json', ['json']], ['application/ld+json', ['jsonld']], + // next 2 are equivalent in ActivityStreams: https://www.w3.org/TR/activitystreams-core/#media-type + ['application/activity+json', ['json']], + ['application/ld+json; profile="https://www.w3.org/ns/activitystreams"', ['json']], ['application/xml', ['xml']], ['application/xhtml+xml', ['xhtml']], ['application/pdf', ['pdf']], diff --git a/src/publish.task.ts b/src/publish.task.ts index 58da6db5ea..a74d6cf74c 100644 --- a/src/publish.task.ts +++ b/src/publish.task.ts @@ -29,7 +29,7 @@ export interface TaskArgs { export const task: Task = { summary: 'bump version, publish to npm, and sync to GitHub', - dev: false, + production: true, run: async ({fs, args, log, invokeTask, dev}): Promise => { const {branch = GIT_DEPLOY_BRANCH, dry = false, restricted = false} = args; if (dry) { @@ -57,7 +57,7 @@ export const task: Task = { // Check in dev mode before proceeding: await buildSource(fs, config, true, log); - await invokeTask('check', {...args, _: []}, undefined, true); + await invokeTask('check', {...args, _: []}, undefined); // Bump the version so the package.json is updated before building: if (!dry) { @@ -68,7 +68,7 @@ export const task: Task = { } // Build to create the final artifacts: - await invokeTask('build', {...args, clean: false}); + await invokeTask('build'); if (dry) { log.info({versionIncrement, publish: config.publish, branch}); diff --git a/src/task/invokeTask.ts b/src/task/invokeTask.ts index b80f1f2313..fbd51924b1 100644 --- a/src/task/invokeTask.ts +++ b/src/task/invokeTask.ts @@ -4,8 +4,10 @@ import {EventEmitter} from 'events'; import {createStopwatch, Timings} from '@feltcoop/felt/util/timings.js'; import {printMs, printTimings} from '@feltcoop/felt/util/print.js'; import {plural} from '@feltcoop/felt/util/string.js'; +import {spawn} from '@feltcoop/felt/util/process.js'; import type {Args} from 'src/task/task.js'; +import {serializeArgs} from '../task/task.js'; import {runTask} from './runTask.js'; import {resolveRawInputPath, getPossibleSourceIds} from '../fs/inputPath.js'; import {TASK_FILE_SUFFIX, isTaskPath, toTaskName} from './task.js'; @@ -52,11 +54,7 @@ export const invokeTask = async ( taskName: string, args: Args, events = new EventEmitter(), - dev?: boolean, ): Promise => { - // TODO not sure about this -- the idea is that imported modules need the updated `NODE_ENV` - process.env['NODE_ENV'] = dev || dev === undefined ? 'development' : 'production'; - const log = new SystemLogger(printLogLabel(taskName || 'gro')); // Check if the caller just wants to see the version. @@ -93,13 +91,6 @@ export const invokeTask = async ( if (await shouldBuildProject(fs, pathData.id)) { // Import these lazily to avoid importing their comparatively heavy transitive dependencies // every time a task is invoked. - if (dev !== undefined) { - // TODO include this? - throw Error( - 'Invalid `invokeTask` call with a `dev` argument and unbuilt project.' + - ' This probably means Gro or a task made something weird happen.', - ); - } log.info('building project to run task'); const timingToLoadConfig = timings.start('load config'); // TODO probably do this as a separate process @@ -129,14 +120,31 @@ export const invokeTask = async ( `→ ${cyan(task.name)} ${(task.mod.task.summary && gray(task.mod.task.summary)) || ''}`, ); const timingToRunTask = timings.start('run task'); - const result = await runTask(fs, task, args, events, invokeTask, dev); - timingToRunTask(); - if (result.ok) { - log.info(`✓ ${cyan(task.name)}`); + const dev = process.env.NODE_ENV !== 'production'; // TODO should this use `fromEnv`? '$app/env'? + if (dev && task.mod.task.production) { + const result = await spawn('npx', ['gro', taskName, ...serializeArgs(args)], { + env: {...process.env, NODE_ENV: 'production'}, + }); + timingToRunTask(); + if (result.ok) { + log.info(`✓ ${cyan(task.name)}`); + } else { + log.info(`${red('🞩')} ${cyan(task.name)}`); + logErrorReasons(log, [ + `spawned task exited with code ${result.code}: ${result.signal}`, + ]); + throw Error('Spawned task failed'); + } } else { - log.info(`${red('🞩')} ${cyan(task.name)}`); - logErrorReasons(log, [result.reason]); - throw result.error; + const result = await runTask(fs, task, args, events, invokeTask); + timingToRunTask(); + if (result.ok) { + log.info(`✓ ${cyan(task.name)}`); + } else { + log.info(`${red('🞩')} ${cyan(task.name)}`); + logErrorReasons(log, [result.reason]); + throw result.error; + } } } else { logErrorReasons(log, loadModulesResult.reasons); diff --git a/src/task/runTask.test.ts b/src/task/runTask.test.ts index 50b639697f..998c5c6b10 100644 --- a/src/task/runTask.test.ts +++ b/src/task/runTask.test.ts @@ -24,7 +24,6 @@ test__runTask('passes args and returns output', async () => { args, new EventEmitter(), async () => {}, - true, ); assert.ok(result.ok); assert.is(result.output, args); @@ -54,7 +53,6 @@ test__runTask('invokes a sub task', async () => { invokedTaskName = invokingTaskName; invokedArgs = invokingArgs; }, - true, ); assert.ok(result.ok); assert.is(invokedTaskName, 'bar/testTask'); @@ -81,7 +79,6 @@ test__runTask('failing task', async () => { {_: []}, new EventEmitter(), async () => {}, - true, ); assert.not.ok(result.ok); assert.ok(result.reason); diff --git a/src/task/runTask.ts b/src/task/runTask.ts index e1982845be..924bfc415d 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -25,19 +25,10 @@ export const runTask = async ( args: Args, events: EventEmitter, invokeTask: typeof InvokeTaskFunction, - dev: boolean | undefined, // `undefined` on first task invocation, so it infers from the first task ): Promise => { const {task} = taskMeta.mod; - if (dev === undefined) { - if (task.dev !== undefined) { - dev = task.dev; - } else { - dev = process.env.NODE_ENV !== 'production'; - } - } - // TODO the `=== false` is needed because we're not normalizing tasks, but we probably should, - // but not in this function, when the task is loaded - if (dev && task.dev === false) { + const dev = process.env.NODE_ENV !== 'production'; // TODO should this use `fromEnv`? '$app/env'? + if (dev && task.production) { throw new TaskError(`The task "${taskMeta.name}" cannot be run in development`); } let output: unknown; @@ -48,13 +39,8 @@ export const runTask = async ( args, events, log: new SystemLogger(printLogLabel(taskMeta.name)), - invokeTask: ( - invokedTaskName, - invokedArgs = args, - invokedEvents = events, - invokedDev = dev, - invokedFs = fs, - ) => invokeTask(invokedFs, invokedTaskName, invokedArgs, invokedEvents, invokedDev), + invokeTask: (invokedTaskName, invokedArgs = args, invokedEvents = events, invokedFs = fs) => + invokeTask(invokedFs, invokedTaskName, invokedArgs, invokedEvents), }); } catch (err) { return { diff --git a/src/task/task.ts b/src/task/task.ts index 557270f81f..314b780f1e 100644 --- a/src/task/task.ts +++ b/src/task/task.ts @@ -7,7 +7,7 @@ import type {Filesystem} from 'src/fs/filesystem.js'; export interface Task { run: (ctx: TaskContext) => Promise; // TODO return value (make generic, forward it..how?) summary?: string; - dev?: boolean; + production?: boolean; } export interface TaskContext { @@ -21,7 +21,6 @@ export interface TaskContext { taskName: string, args?: Args, events?: StrictEventEmitter, - dev?: boolean, fs?: Filesystem, ) => Promise; } @@ -51,3 +50,17 @@ export interface Args { _: string[]; [key: string]: unknown; // can assign anything to `args` in tasks } + +export const serializeArgs = (args: Args): string[] => { + const result: string[] = []; + let _: string[] | null = null; + for (const [key, value] of Object.entries(args)) { + if (key === '_') { + _ = (value as any[]).map((v) => v.toString()); + } else { + result.push(`--${key}`); + result.push((value as any).toString()); + } + } + return _ ? [...result, ..._] : result; +};