diff --git a/packages/storage/README.md b/packages/storage/README.md new file mode 100644 index 0000000000..935c200a3a --- /dev/null +++ b/packages/storage/README.md @@ -0,0 +1,34 @@ +# @astrojs/studio + +This package manages the connection between a local Astro project and [Astro Studio](studio). At this time, this package is not intended for direct use by end users, but rather as a dependency of other Astro packages. + +## Support + +- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! + +- Check our [Astro Integration Documentation][astro-integration] for more on integrations. + +- Submit bug reports and feature requests as [GitHub issues][issues]. + +## Contributing + +This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: + +- [Contributor Manual][contributing] +- [Code of Conduct][coc] +- [Community Guide][community] + +## License + +MIT + +Copyright (c) 2023–present [Astro][astro] + +[astro]: https://astro.build/ +[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md +[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md +[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md +[discord]: https://astro.build/chat/ +[issues]: https://github.com/withastro/astro/issues +[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ +[studio]: https://studio.astro.build/ diff --git a/packages/storage/package.json b/packages/storage/package.json new file mode 100644 index 0000000000..b897888482 --- /dev/null +++ b/packages/storage/package.json @@ -0,0 +1,48 @@ +{ + "name": "@astrojs/storage", + "version": "0.1.0", + "description": "Add libSQL and Astro Studio support to your Astro site", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/storage" + }, + "bugs": "https://github.com/withastro/astro/issues", + "homepage": "https://docs.astro.build/en/guides/integrations-guide/studio/", + "type": "module", + "author": "withastro", + "types": "./dist/index.js", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "keywords": [ + "withastro", + "astro-integration" + ], + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "build:ci": "astro-scripts build \"src/**/*.ts\"", + "dev": "astro-scripts dev \"src/**/*.ts\"" + }, + "dependencies": { + "ci-info": "^4.0.0", + "kleur": "^4.1.5", + "ora": "^8.0.1", + "@astrojs/studio": "^0.1.0" + }, + "devDependencies": { + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/packages/storage/src/codegen.ts b/packages/storage/src/codegen.ts new file mode 100644 index 0000000000..e27d4942c3 --- /dev/null +++ b/packages/storage/src/codegen.ts @@ -0,0 +1,75 @@ +import type { AstroConfig } from 'astro'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { getAstroStudioStorageUrl } from './utils.js'; +import { STORAGE_CODE_FILE, STORAGE_TYPES_FILE } from './consts.js'; +import { getProjectIdFromFile, getSessionIdFromFile, safeFetch } from '@astrojs/studio'; + +export async function codegen(astroConfig: Pick) { + await codegenInternal({ root: astroConfig.root }); +} + +async function codegenInternal({ root }: { root: URL }) { + const dotAstroDir = new URL('.astro/', root); + + const images = await storageRequest('image'); + const all = await storageRequest('all'); + + const code = ` + // This file is auto-generated by Astro Studio. Do not modify this file directly. + export const images = ${JSON.stringify(images)}; + export const all = ${JSON.stringify(all)}; + + export type Image = keyof typeof images | string & {}; + export type File = keyof typeof all | string & {}; + `; + + const types = ` + // This file is auto-generated by Astro Studio. Do not modify this file directly. + declare module 'astro:storage' { + export function getFile(name: import("./${STORAGE_CODE_FILE}").File); + export function getStudioImage(name: import("./${STORAGE_CODE_FILE}").Image); + } + ` + + if (!existsSync(dotAstroDir)) { + await mkdir(dotAstroDir); + } + + await writeFile(new URL(STORAGE_CODE_FILE, dotAstroDir), code); + await writeFile(new URL(STORAGE_TYPES_FILE, dotAstroDir), types); +} + +async function storageRequest(fileKind: 'all' | 'image' = 'all') { + const projectId = await getProjectIdFromFile(); + const linkUrl = getAstroStudioStorageUrl(); + const sessionToken = await getSessionIdFromFile(); + + const response = await safeFetch( + linkUrl, + { + method: 'POST', + headers: { + Authorization: `Bearer ${sessionToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ projectId, fileKind }), + }, + (res) => { + // Unauthorized + if (res.status === 401) { + } + } + ); + + return groupDataByName((await response.json()).data); +} + +function groupDataByName(data: any) { + const grouped: Record = {}; + for (const item of data) { + grouped[item.name] = item; + } + + return grouped; +} diff --git a/packages/storage/src/consts.ts b/packages/storage/src/consts.ts new file mode 100644 index 0000000000..2bf66a7814 --- /dev/null +++ b/packages/storage/src/consts.ts @@ -0,0 +1,2 @@ +export const STORAGE_CODE_FILE = 'storage.ts'; +export const STORAGE_TYPES_FILE = 'storage-types.d.ts'; diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts new file mode 100644 index 0000000000..bd32f286b4 --- /dev/null +++ b/packages/storage/src/index.ts @@ -0,0 +1 @@ +export { storageIntegration as default } from './integration.js' diff --git a/packages/storage/src/integration.ts b/packages/storage/src/integration.ts new file mode 100644 index 0000000000..028a2d7dd4 --- /dev/null +++ b/packages/storage/src/integration.ts @@ -0,0 +1,115 @@ +import type { AstroConfig, AstroIntegration, AstroIntegrationLogger } from "astro"; +import { STORAGE_CODE_FILE, STORAGE_TYPES_FILE } from "./consts.js"; +import { codegen } from "./codegen.js"; +import { readFile, writeFile } from "node:fs/promises"; +import { bold } from "kleur/colors"; +import path from "node:path"; +import { fileURLToPath } from "url"; +import { normalizePath } from "vite"; +import { existsSync } from "node:fs"; + +const VIRTUAL_MODULE_ID = 'astro:storage'; +const RESOLVED_MODULE_ID = '\0' + 'astro:storage'; + +type VitePlugin = Required['plugins'][number]; + +function vitePluginStorage({ root }: { root: URL }): VitePlugin { + const dotAstroDir = new URL('.astro/', root); + + return { + name: 'astro:storage', + async resolveId(id) { + if (id !== VIRTUAL_MODULE_ID) return; + return RESOLVED_MODULE_ID; + }, + async load(id) { + if (id !== RESOLVED_MODULE_ID) return; + + return ` + import { images, all } from '${new URL(STORAGE_CODE_FILE, dotAstroDir)}'; + + export function getFile(name) { + return all[name]; + } + + export function getStudioImage(name) { + return images[name].id; + } + ` + } + } +} + +export function storageIntegration(): AstroIntegration { +return { + name: "astro:studio", + hooks: { + 'astro:config:setup': async ({ config, updateConfig, logger }) => { + updateConfig({ + vite: { + plugins: [vitePluginStorage({ root: config.root }), vitePluginInjectEnvTs({ srcDir: config.srcDir, root: config.root }, logger)] + } + }) + }, + 'astro:config:done': async ({ config }) => { + await codegen({ root: config.root }); + } + } +} +} + +export function vitePluginInjectEnvTs( + { srcDir, root }: { srcDir: URL; root: URL }, + logger: AstroIntegrationLogger +): VitePlugin { + return { + name: 'storage-inject-env-ts', + enforce: 'post', + async config() { + await setUpEnvTs({ srcDir, root, logger }); + }, + }; +} + +export async function setUpEnvTs({ + srcDir, + root, + logger, +}: { + srcDir: URL; + root: URL; + logger: AstroIntegrationLogger; +}) { + const envTsPath = getEnvTsPath({ srcDir }); + const envTsPathRelativetoRoot = normalizePath( + path.relative(fileURLToPath(root), fileURLToPath(envTsPath)) + ); + + if (existsSync(envTsPath)) { + let typesEnvContents = await readFile(envTsPath, 'utf-8'); + const dotAstroDir = new URL('.astro/', root); + + if (!existsSync(dotAstroDir)) return; + + const dbTypeReference = getStorageTypeReference({ srcDir, dotAstroDir }); + + if (!typesEnvContents.includes(dbTypeReference)) { + typesEnvContents = `${dbTypeReference}\n${typesEnvContents}`; + await writeFile(envTsPath, typesEnvContents, 'utf-8'); + logger.info(`Added ${bold(envTsPathRelativetoRoot)} types`); + } + } +} + +function getStorageTypeReference({ srcDir, dotAstroDir }: { srcDir: URL; dotAstroDir: URL }) { + const storageTypesFile = new URL(STORAGE_TYPES_FILE, dotAstroDir); + const storageTypesRelativeToSrcDir = normalizePath( + path.relative(fileURLToPath(srcDir), fileURLToPath(storageTypesFile)) + ); + + return `/// `; +} + +function getEnvTsPath({ srcDir }: { srcDir: URL }) { + return new URL('env.d.ts', srcDir); +} diff --git a/packages/storage/src/utils.ts b/packages/storage/src/utils.ts new file mode 100644 index 0000000000..2369810e07 --- /dev/null +++ b/packages/storage/src/utils.ts @@ -0,0 +1,5 @@ +import { getAstroStudioUrl } from "@astrojs/studio"; + +export function getAstroStudioStorageUrl(): string { + return getAstroStudioUrl() + '/api/cli/files.list'; +} diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json new file mode 100644 index 0000000000..18443cddf2 --- /dev/null +++ b/packages/storage/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98e1b9d291..708e9724bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5536,6 +5536,34 @@ importers: specifier: ^2.0.0 version: 2.0.0 + packages/storage: + dependencies: + '@astrojs/studio': + specifier: ^0.1.0 + version: link:../studio + ci-info: + specifier: ^4.0.0 + version: 4.0.0 + kleur: + specifier: ^4.1.5 + version: 4.1.5 + ora: + specifier: ^8.0.1 + version: 8.0.1 + devDependencies: + astro: + specifier: workspace:* + version: link:../astro + astro-scripts: + specifier: workspace:* + version: link:../../scripts + typescript: + specifier: ^5.4.5 + version: 5.4.5 + vite: + specifier: ^5.2.11 + version: 5.2.11(@types/node@18.19.31)(sass@1.77.1) + packages/studio: dependencies: ci-info: