Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change .well-known/package.json to use the static directory #428

Merged
merged 21 commits into from
Nov 9, 2023
5 changes: 5 additions & 0 deletions .changeset/young-suns-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@grogarden/gro': patch
---

change `.well-known/package.json` and `.nojekyll` to use the static directory
185 changes: 128 additions & 57 deletions src/lib/gro_plugin_sveltekit_frontend.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {spawn_process, type Spawned_Process} from '@grogarden/util/process.js';
import {mkdir, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
import {mkdir, rm, writeFile} from 'node:fs/promises';
import {dirname, join} from 'node:path';
import type {Config as SveltekitConfig} from '@sveltejs/kit';

import type {Plugin, Plugin_Context} from './plugin.js';
import {print_command_args, serialize_args, to_forwarded_args} from './args.js';
import {SVELTEKIT_BUILD_DIRNAME} from './paths.js';
import {exists} from './exists.js';
import {
serialize_package_json,
Expand All @@ -27,20 +27,26 @@ export interface Options {
* If a function, maps the value.
*/
well_known_package_json?: boolean | Map_Package_Json;

/**
* Optional SvelteKit config, defaults to `svelte.config.js`.
*/
sveltekit_config?: string | SveltekitConfig;
}

export type Host_Target = 'github_pages' | 'static' | 'node';

const output_dir = SVELTEKIT_BUILD_DIRNAME;

export const plugin = ({
host_target = 'github_pages',
well_known_package_json,
sveltekit_config,
}: Options = {}): Plugin<Plugin_Context> => {
let sveltekit_process: Spawned_Process | null = null;
return {
name: 'gro_plugin_sveltekit_frontend',
setup: async ({dev, watch, log}) => {
const {assets_path} = await init_sveltekit_config(sveltekit_config);

if (dev) {
// `vite dev` in development mode
if (watch) {
Expand All @@ -55,24 +61,55 @@ export const plugin = ({
}
} else {
// `vite build` in production mode
const serialized_args = ['build', ...serialize_args(to_forwarded_args('vite'))];
log.info(print_command_args(['vite'].concat(serialized_args)));
const spawned = await spawn_cli('vite', serialized_args); // TODO call with the gro helper instead of npx?
if (!spawned?.ok) {
throw new Task_Error('vite build failed with exit code ' + spawned?.code);

const package_json = await load_mapped_package_json();
if (well_known_package_json === undefined) {
well_known_package_json = package_json.public; // eslint-disable-line no-param-reassign
}
}
},
adapt: async () => {
if (host_target === 'github_pages') {
await ensure_nojekyll(output_dir);
}
const mapped_package_json = !well_known_package_json
? null
: well_known_package_json === true
? package_json
: await well_known_package_json(package_json);
const serialized_package_json =
mapped_package_json && serialize_package_json(mapped_package_json);

// TODO doing this here makes `static/.well-known/package.json` unavailable to Vite plugins,
// so we may want to do a more complicated temporary copy
// into `static/` before `vite build` in `setup`,
// and afterwards it would delete the files but only if they didn't already exist
await ensure_well_known_package_json(well_known_package_json, output_dir);
// TODO this strategy means the files aren't available during development --
// maybe a Vite middleware is best? what if this plugin added its plugin to your `vite.config.ts`?

// copy files to `static` before building, in such a way
// that's non-destructive to existing files and dirs and easy to clean up
const cleanups: Cleanup[] = [
serialized_package_json
? await create_temporarily(
join(assets_path, '.well-known/package.json'),
serialized_package_json,
)
: null!,
/**
* GitHub pages processes everything with Jekyll by default,
* breaking things like files and dirs prefixed with an underscore.
* This adds a `.nojekyll` file to the root of the output
* to tell GitHub Pages to treat the outputs as plain static files.
*/
host_target === 'github_pages'
? await create_temporarily(join(assets_path, '.nojekyll'), '')
: null!,
].filter(Boolean);
const cleanup = () => Promise.all(cleanups.map((c) => c()));
try {
const serialized_args = ['build', ...serialize_args(to_forwarded_args('vite'))];
log.info(print_command_args(['vite'].concat(serialized_args)));
const spawned = await spawn_cli('vite', serialized_args); // TODO call with the gro helper instead of npx?
if (!spawned?.ok) {
throw new Task_Error('vite build failed with exit code ' + spawned?.code);
}
} catch (err) {
await cleanup();
throw err;
}
await cleanup();
}
},
teardown: async () => {
if (sveltekit_process) {
Expand All @@ -83,49 +120,83 @@ export const plugin = ({
};
};

const NOJEKYLL_FILENAME = '.nojekyll';
interface Cleanup {
(): Promise<void>;
}

// TODO for `src` and `src.json`
// const copy_temporarily = async (
// source_path: string,
// dest_dir: string,
// dest_base_dir = '',
// ): Promise<Cleanup> => {
// const path = join(dest_dir, dest_base_dir, source_path);
// const dir = dirname(path);

// const dir_already_exists = await exists(dir);
// if (!dir_already_exists) {
// await mkdir(dir, {recursive: true});
// }

// const path_already_exists = await exists(path);
// if (!path_already_exists) {
// await cp(source_path, path, {recursive: true});
// }
// return async () => {
// if (!dir_already_exists) {
// await rm(dir, {recursive: true});
// } else if (!path_already_exists) {
// await rm(path, {recursive: true});
// }
// };
// };

/**
* GitHub pages processes everything with Jekyll by default,
* breaking things like files and dirs prefixed with an underscore.
* This adds a `.nojekyll` file to the root of the output
* to tell GitHub Pages to treat the outputs as plain static files.
* Creates a file at `path` with `contents` if it doesn't already exist,
* and returns a function that deletes the file and any created directories.
* @param path
* @param contents
* @returns cleanup function that deletes the file and any created dirs
*/
const ensure_nojekyll = async (dir: string): Promise<void> => {
const path = `${dir}/${NOJEKYLL_FILENAME}`;
if (!(await exists(path))) {
const create_temporarily = async (path: string, contents: string): Promise<Cleanup> => {
// TODO handle more than 1 hardcoded depth
const dir = dirname(path);
const dir_already_exists = await exists(dir);
let root_created_dir: string | undefined;
if (!dir_already_exists) {
root_created_dir = await to_root_dir_that_doesnt_exist(dir);
if (!root_created_dir) throw Error();
await mkdir(dir, {recursive: true});
await writeFile(path, '', 'utf8');
}

const path_already_exists = await exists(path);
if (!path_already_exists) {
await writeFile(path, contents, 'utf8');
}

return async () => {
if (!dir_already_exists) {
if (!root_created_dir) throw Error();
await rm(root_created_dir, {recursive: true});
} else if (!path_already_exists) {
await rm(path);
}
};
};

/**
* Outputs `${dir}/.well-known/package.json` if it doesn't already exist.
* @param well_known_package_json - if `undefined`, inferred to be `true` if `pkg.public` is truthy
* @param output_dir
* Niche and probably needs refactoring,
* for `/a/b/DOESNT_EXIST/NOR_THIS/ETC` returns `/a/b/DOESNT_EXIST`.
*/
const ensure_well_known_package_json = async (
well_known_package_json: boolean | Map_Package_Json | undefined,
output_dir: string,
): Promise<void> => {
const package_json = await load_mapped_package_json();

if (well_known_package_json === undefined) {
well_known_package_json = package_json.public; // eslint-disable-line no-param-reassign
}
if (!well_known_package_json) return;

const mapped =
well_known_package_json === true ? package_json : await well_known_package_json(package_json);
if (!mapped) return;

const svelte_config = await init_sveltekit_config(); // TODO param
const well_known_dir = join(output_dir, svelte_config.assets_path, '..', '.well-known');
const path = join(well_known_dir, 'package.json');
if (await exists(path)) return; // don't clobber
if (!(await exists(well_known_dir))) {
await mkdir(well_known_dir, {recursive: true});
}
const new_contents = serialize_package_json(mapped);
await writeFile(path, new_contents, 'utf8');
const to_root_dir_that_doesnt_exist = async (dir: string): Promise<string | undefined> => {
let prev: string | undefined;
let d = dir;
do {
// eslint-disable-next-line no-await-in-loop
if (await exists(d)) {
return prev;
}
prev = d;
} while ((d = dirname(d)));
throw Error('no dirs exist for ' + dir);
};
11 changes: 7 additions & 4 deletions src/lib/sveltekit_config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Config} from '@sveltejs/kit';
import type {Config as SveltekitConfig} from '@sveltejs/kit';
import type {CompileOptions, PreprocessorGroup} from 'svelte/compiler';
import {join} from 'node:path';
import {cwd} from 'node:process';
Expand All @@ -9,7 +9,9 @@ import {SVELTEKIT_CONFIG_FILENAME} from './paths.js';
* Loads a SvelteKit config at `dir`.
* @returns
*/
export const load_sveltekit_config = async (dir: string = cwd()): Promise<Config | null> => {
export const load_sveltekit_config = async (
dir: string = cwd(),
): Promise<SveltekitConfig | null> => {
try {
return (await import(join(dir, SVELTEKIT_CONFIG_FILENAME))).default;
} catch (err) {
Expand All @@ -27,7 +29,7 @@ export const load_sveltekit_config = async (dir: string = cwd()): Promise<Config
*/
export interface Parsed_Sveltekit_Config {
// TODO probably fill these out with defaults
sveltekit_config: Config | null;
sveltekit_config: SveltekitConfig | null;
alias: Record<string, string> | undefined;
base_url: '' | `/${string}` | undefined;
assets_url: '' | `http://${string}` | `https://${string}` | undefined;
Expand All @@ -44,13 +46,14 @@ export interface Parsed_Sveltekit_Config {
svelte_preprocessors: PreprocessorGroup | PreprocessorGroup[] | undefined;
}

// TODO currently incomplete and hack - maybe rethink
/**
* Returns Gro-relevant properties of a SvelteKit config
* as a convenience wrapper around `load_sveltekit_config`.
* Needed because SvelteKit doesn't expose its config resolver.
*/
export const init_sveltekit_config = async (
dir_or_config: string | Config = cwd(),
dir_or_config: string | SveltekitConfig = cwd(),
): Promise<Parsed_Sveltekit_Config> => {
const sveltekit_config =
typeof dir_or_config === 'string' ? await load_sveltekit_config(dir_or_config) : dir_or_config;
Expand Down
Loading