@@ -66,6 +66,7 @@ | |||
"@clack/prompts": "^0.7.0", | |||
"@modal-sh/patchouli-core": "workspace:*", | |||
"pino": "^8.20.0", | |||
"valibot": "^0.30.0", | |||
"yargs": "^17.7.2" | |||
} | |||
} |
@@ -1,22 +1,27 @@ | |||
import { Cli } from './packages/cli-wrapper'; | |||
import { AdderController, AdderControllerImpl } from './modules/adder'; | |||
import { BindController, BindControllerImpl } from './modules/bind'; | |||
export const addCommands = (cli: Cli) => { | |||
const adderController: AdderController = new AdderControllerImpl(); | |||
const bindController: BindController = new BindControllerImpl(); | |||
return cli | |||
.command({ | |||
aliases: ['a'], | |||
command: 'add', | |||
parameters: ['<a> <b>'], | |||
describe: 'Add two numbers', | |||
handler: adderController.addNumbers, | |||
}) | |||
.command({ | |||
aliases: ['s'], | |||
command: 'subtract', | |||
parameters: ['<a> <b>'], | |||
describe: 'Subtract two numbers', | |||
handler: adderController.subtractNumbers, | |||
aliases: ['b'], | |||
command: 'bind', | |||
parameters: [], | |||
describe: 'Binds a collection of static Web assets into a book.', | |||
handler: bindController.bindBook, | |||
options: { | |||
'format': { | |||
alias: 'f', | |||
describe: 'The format of the output.', | |||
type: 'string', | |||
}, | |||
'outputPath': { | |||
alias: 'o', | |||
describe: 'The destination of the output.', | |||
type: 'string', | |||
} | |||
}, | |||
}); | |||
}; |
@@ -1,9 +1,9 @@ | |||
import { addCommands } from '@/commands'; | |||
import { createCli } from '@/cli'; | |||
import { addCommands } from './commands'; | |||
import { createCli } from './cli'; | |||
import { hideBin } from 'yargs/helpers'; | |||
const cli = createCli({ | |||
name: 'cli', | |||
name: 'patchouli', | |||
}); | |||
addCommands(cli); | |||
@@ -1,82 +0,0 @@ | |||
import { | |||
AdderService, | |||
AdderServiceImpl, | |||
ArgumentOutOfRangeError, | |||
InvalidArgumentTypeError, | |||
} from './adder.service'; | |||
import { CommandHandler } from '../../packages/cli-wrapper'; | |||
export interface AdderController { | |||
addNumbers: CommandHandler; | |||
subtractNumbers: CommandHandler; | |||
} | |||
export class AdderControllerImpl implements AdderController { | |||
constructor( | |||
private readonly adderService: AdderService = new AdderServiceImpl(), | |||
) { | |||
// noop | |||
} | |||
readonly addNumbers: CommandHandler = (params) => { | |||
if (!params.interactive) { | |||
const checkArgs = params.args as Record<string, unknown>; | |||
if (typeof checkArgs.a === 'undefined') { | |||
params.logger.error('Missing required argument: a'); | |||
return -1; | |||
} | |||
if (typeof checkArgs.b === 'undefined') { | |||
params.logger.error('Missing required argument: b'); | |||
return -1; | |||
} | |||
} | |||
const { a, b } = params.args; | |||
try { | |||
const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) }); | |||
params.logger.info(response); | |||
} catch (errorRaw) { | |||
const error = errorRaw as Error; | |||
params.logger.error(error.message); | |||
if (error instanceof InvalidArgumentTypeError) { | |||
return -1; | |||
} | |||
if (error instanceof ArgumentOutOfRangeError) { | |||
return -2; | |||
} | |||
return -3; | |||
} | |||
return 0; | |||
} | |||
readonly subtractNumbers: CommandHandler = (params) => { | |||
if (!params.interactive) { | |||
const checkArgs = params.args as Record<string, unknown>; | |||
if (typeof checkArgs.a === 'undefined') { | |||
params.logger.error('Missing required argument: a'); | |||
return -1; | |||
} | |||
if (typeof checkArgs.b === 'undefined') { | |||
params.logger.error('Missing required argument: b'); | |||
return -1; | |||
} | |||
} | |||
const { a, b } = params.args; | |||
try { | |||
const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) }); | |||
params.logger.info(response); | |||
} catch (errorRaw) { | |||
const error = errorRaw as Error; | |||
params.logger.error(error.message); | |||
if (error instanceof InvalidArgumentTypeError) { | |||
return -1; | |||
} | |||
if (error instanceof ArgumentOutOfRangeError) { | |||
return -2; | |||
} | |||
return -3; | |||
} | |||
return 0; | |||
} | |||
} |
@@ -1,21 +0,0 @@ | |||
import { | |||
add, | |||
AddFunctionOptions, | |||
ArgumentOutOfRangeError, | |||
InvalidArgumentTypeError, | |||
} from '@modal-sh/patchouli-core'; | |||
export interface AdderService { | |||
addNumbers(options: AddFunctionOptions): number; | |||
} | |||
export class AdderServiceImpl implements AdderService { | |||
addNumbers(options: AddFunctionOptions) { | |||
return add(options); | |||
} | |||
} | |||
export { | |||
ArgumentOutOfRangeError, | |||
InvalidArgumentTypeError, | |||
}; |
@@ -1,2 +0,0 @@ | |||
export * from './adder.controller'; | |||
export * from './adder.service'; |
@@ -0,0 +1,55 @@ | |||
import { | |||
BindService, | |||
BindServiceImpl, | |||
} from './bind.service'; | |||
import { CommandHandler } from '../../packages/cli-wrapper'; | |||
export interface BindController { | |||
bindBook: CommandHandler; | |||
} | |||
export class BindControllerImpl implements BindController { | |||
constructor( | |||
private readonly bindService: BindService = new BindServiceImpl(), | |||
) { | |||
// noop | |||
} | |||
readonly bindBook: CommandHandler = async (params) => { | |||
if (!params.interactive) { | |||
const checkArgs = params.args as Record<string, unknown>; | |||
const checkFormat = checkArgs.format ?? checkArgs.f; | |||
if (typeof checkFormat === 'undefined') { | |||
params.logger.error('Missing required argument: format'); | |||
return -1; | |||
} | |||
const checkOutputPath = checkArgs.outputPath ?? checkArgs.o; | |||
if (typeof checkOutputPath === 'undefined') { | |||
params.logger.error('Missing required argument: outputPath'); | |||
return -1; | |||
} | |||
} | |||
const { inputPath: inputPathRaw, f, format = f, o, outputPath = o } = params.args; | |||
const inputPath = inputPathRaw ?? process.cwd(); | |||
try { | |||
const response = await this.bindService.bindBook({ | |||
input: { | |||
path: inputPath, | |||
}, | |||
output: { | |||
format, | |||
path: outputPath | |||
}, | |||
}); | |||
params.logger.info(response); | |||
} catch (errorRaw) { | |||
console.error(errorRaw); | |||
const error = errorRaw as Error; | |||
params.logger.error(error.message); | |||
return -2; | |||
} | |||
return 0; | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
import { | |||
bindBook, | |||
BindFunctionOptions, | |||
} from '@modal-sh/patchouli-core'; | |||
import * as v from 'valibot'; | |||
import { writeFile } from 'fs/promises'; | |||
import { resolve } from 'path'; | |||
const outputOptionSchema = v.object({ | |||
path: v.string() | |||
}); | |||
interface BindOptions extends BindFunctionOptions { | |||
output: BindFunctionOptions['output'] & v.Output<typeof outputOptionSchema>; | |||
} | |||
export interface BindService { | |||
bindBook(options: BindOptions): Promise<string>; | |||
} | |||
export class BindServiceImpl implements BindService { | |||
async bindBook(options: BindOptions) { | |||
const parsedOptions = await v.parseAsync(outputOptionSchema, options.output); | |||
const effectiveOptions = { | |||
...options, | |||
output: { | |||
...options.output, | |||
...parsedOptions, | |||
}, | |||
}; | |||
const buffer = await bindBook(effectiveOptions); | |||
const outputPath = effectiveOptions.output.path; | |||
await writeFile(outputPath, buffer); | |||
const absolutePath = resolve(outputPath); | |||
return `File successfully written to ${absolutePath}.`; | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
export * from './bind.controller'; | |||
export * from './bind.service'; |
@@ -23,6 +23,13 @@ export interface CommandArgs { | |||
describe: string; | |||
handler: CommandHandler; | |||
interactiveMode?: boolean; | |||
options?: Record<string, { | |||
alias?: string, | |||
describe: string, | |||
type: string, | |||
default?: unknown, | |||
hidden?: boolean, | |||
}>; | |||
} | |||
type PromptValueArgs = [Record<string, string>, ...never[]] | |||
@@ -265,7 +272,9 @@ export class Cli { | |||
const commandArgs = { | |||
...args, | |||
command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`, | |||
command: parameters.length > 0 | |||
? `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}` | |||
: command, | |||
usage: parameters.map((p) => `${command} ${p}`).join('\n'), | |||
handler: this.buildHandler(handler, { | |||
option, | |||
@@ -1,7 +1,7 @@ | |||
import { describe, it, expect, vi, beforeAll } from 'vitest'; | |||
import { createCli } from '../src/cli'; | |||
import { addCommands } from '../src/commands'; | |||
import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder'; | |||
import { BindServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/bind'; | |||
import { Cli } from '../src/packages/cli-wrapper'; | |||
vi.mock('process'); | |||
@@ -22,7 +22,7 @@ describe('cli', () => { | |||
}); | |||
it('returns error when given invalid inputs', async () => { | |||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
vi.spyOn(BindServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
throw new InvalidArgumentTypeError('Invalid input'); | |||
}); | |||
@@ -31,7 +31,7 @@ describe('cli', () => { | |||
}); | |||
it('returns error when given out-of-range inputs', async () => { | |||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
vi.spyOn(BindServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
throw new ArgumentOutOfRangeError('Out of range'); | |||
}); | |||
@@ -40,7 +40,7 @@ describe('cli', () => { | |||
}); | |||
it('returns error when an unexpected error occurs', async () => { | |||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
vi.spyOn(BindServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||
throw new Error('Unexpected error'); | |||
}); | |||
@@ -8,6 +8,9 @@ | |||
"engines": { | |||
"node": ">=12" | |||
}, | |||
"bin": { | |||
"patchouli": "./dist/cjs/production/index.js" | |||
}, | |||
"license": "UNLICENSED", | |||
"keywords": [ | |||
"pridepack" | |||
@@ -60,5 +63,8 @@ | |||
}, | |||
"typesVersions": { | |||
"*": {} | |||
}, | |||
"dependencies": { | |||
"valibot": "^0.30.0" | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
import * as v from 'valibot'; | |||
export const inputSchema = v.object({}, v.unknown()); | |||
export type Input = v.Output<typeof inputSchema>; | |||
export const BOOK_FILENAME = 'patchouli.book.json' as const; | |||
export const BINDING_FILENAME = 'patchouli.binding.json' as const; | |||
export const bindingFileBaseSchema = v.object({ | |||
generatorType: v.string(), | |||
generatorConfigFilePath: v.optional(v.string()), | |||
generatorDistDirectory: v.string(), | |||
pageOrdering: v.array(v.string()), // allow blobs on page ordering | |||
}); | |||
export const bookFileSchema = v.object({ | |||
title: v.string(), | |||
publisher: v.optional(v.string()), | |||
isbn: v.optional(v.string()), | |||
id: v.optional(v.string()), | |||
creator: v.string(), | |||
contributors: v.optional(v.array(v.string())), | |||
description: v.optional(v.string()), | |||
subjects: v.optional(v.array(v.string())), | |||
rights: v.optional(v.string()), | |||
}); |
@@ -0,0 +1,44 @@ | |||
import * as v from 'valibot'; | |||
import {Input} from '../../common'; | |||
export const name = 'archive' as const; | |||
const inputSchema = v.object({ | |||
blob: v.blob(), | |||
type: v.string(), | |||
}); | |||
interface ArchiveInput extends Input, v.Output<typeof inputSchema> {} | |||
export class InvalidArchiveTypeError extends Error {} | |||
const extractZip = () => { | |||
}; | |||
const extractGzip = () => { | |||
}; | |||
const extractTar = () => { | |||
}; | |||
export const compileFromInput = async <T extends ArchiveInput = ArchiveInput>(input: T) => { | |||
switch (input.type) { | |||
// TODO get files from archive type | |||
case 'zip': { | |||
return extractZip(); | |||
} | |||
case 'gzip': { | |||
return extractGzip(); | |||
} | |||
case 'tar': { | |||
return extractTar(); | |||
} | |||
default: | |||
break; | |||
} | |||
throw new InvalidArchiveTypeError('Input type is invalid'); | |||
}; |
@@ -0,0 +1,66 @@ | |||
import {readdir, stat, readFile} from 'fs/promises'; | |||
import {resolve} from 'path'; | |||
import * as v from 'valibot'; | |||
import {BINDING_FILENAME, bindingFileBaseSchema, BOOK_FILENAME, bookFileSchema, Input} from '../../common'; | |||
export const name = 'path' as const; | |||
const inputSchema = v.object({ | |||
path: v.string(), | |||
}); | |||
interface PathInput extends Input, v.Output<typeof inputSchema> {} | |||
const readPath = async (path: string, rootPath = path, readFiles = []) => { | |||
const files = await readdir(path); | |||
// TODO get the buffers on the tree | |||
//console.log(files); | |||
}; | |||
export class InvalidInputPathError extends Error {} | |||
type Book = v.Output<typeof bookFileSchema>; | |||
type Binding = v.Output<typeof bindingFileBaseSchema>; | |||
const getBookFile = async (bookFilePath: string): Promise<Book | undefined> => { | |||
const bookFileString = await readFile(bookFilePath, 'utf-8'); | |||
const bookFileRaw = JSON.parse(bookFileString); | |||
return await v.parseAsync(bookFileSchema, bookFileRaw); | |||
}; | |||
const getBindingFile = async (bindingFilePath: string, defaultBinding: Binding) => { | |||
const bindingFileString = await readFile(bindingFilePath, 'utf-8'); | |||
const bindingFileRaw = JSON.parse(bindingFileString); | |||
return { | |||
...defaultBinding, | |||
...bindingFileRaw, | |||
}; | |||
}; | |||
export const compileFromInput = async <T extends PathInput = PathInput>(input: T) => { | |||
const files = await readdir(input.path); | |||
if (!files.includes(BOOK_FILENAME)) { | |||
throw new InvalidInputPathError(`Path does not contain a "${BOOK_FILENAME}" file.`); | |||
} | |||
const bookFilePath = resolve(input.path, BOOK_FILENAME); | |||
const bookFile = await getBookFile(bookFilePath); | |||
const defaultBinding = { | |||
generatorType: 'static', | |||
// TODO should make the dist directory related to book file when getting contents | |||
generatorDistDirectory: resolve(input.path, 'dist'), | |||
}; | |||
const bindingFilePath = resolve(input.path, BINDING_FILENAME); | |||
const bindingFile = files.includes(BINDING_FILENAME) | |||
? await getBindingFile(bindingFilePath, defaultBinding) | |||
: defaultBinding; | |||
return [ | |||
]; | |||
}; |
@@ -0,0 +1,7 @@ | |||
import {Input} from '../../common'; | |||
export const name = 'epub' as const; | |||
export const bindBook = async <T extends Input = Input>(input: T) => { | |||
return Buffer.from(input.path + ' ' + name); | |||
}; |
@@ -0,0 +1,7 @@ | |||
import {Input} from '../../common'; | |||
export const name = 'pdf' as const; | |||
export const bindBook = async <T extends Input = Input>(input: T) => { | |||
return Buffer.from(input.path + ' ' + name); | |||
}; |
@@ -1,34 +1,52 @@ | |||
export interface AddFunctionOptions { | |||
a: number; | |||
b: number; | |||
} | |||
import * as v from 'valibot'; | |||
import assert from 'assert'; | |||
import * as PdfFormat from './formats/pdf'; | |||
import * as EpubFormat from './formats/epub'; | |||
import * as PathCompiler from './compilers/path'; | |||
import {inputSchema} from './common'; | |||
export interface AddFunction { | |||
(options: AddFunctionOptions): number; | |||
} | |||
const AVAILABLE_COMPILERS = [ | |||
PathCompiler, | |||
]; | |||
export class InvalidArgumentTypeError extends TypeError { | |||
constructor(message: string) { | |||
super(message); | |||
this.name = 'InvalidArgumentTypeError'; | |||
} | |||
} | |||
const AVAILABLE_FORMATS = [ | |||
PdfFormat, | |||
EpubFormat | |||
]; | |||
export class ArgumentOutOfRangeError extends RangeError { | |||
constructor(message: string) { | |||
super(message); | |||
this.name = 'ArgumentOutOfRangeError'; | |||
} | |||
const optionsSchema = v.object( | |||
{ | |||
input: v.merge( | |||
[ | |||
inputSchema, | |||
v.object({ | |||
sourceType: v.picklist(AVAILABLE_COMPILERS.map((f) => f.name)), | |||
}) | |||
], | |||
v.unknown(), | |||
), | |||
output: v.object( | |||
{ | |||
format: v.picklist(AVAILABLE_FORMATS.map((f) => f.name)), | |||
}, | |||
v.unknown(), | |||
), | |||
}, | |||
v.never(), | |||
) | |||
export interface BindFunctionOptions extends v.Output<typeof optionsSchema> {} | |||
export interface BindFunction { | |||
<T extends BindFunctionOptions = BindFunctionOptions>(options: T): Promise<Buffer>; | |||
} | |||
export const add: AddFunction = (options: AddFunctionOptions): number => { | |||
const { a, b } = options as unknown as Record<string, unknown>; | |||
if (typeof a !== 'number' || typeof b !== 'number') { | |||
throw new InvalidArgumentTypeError('a and b must be numbers'); | |||
} | |||
if (!Number.isFinite(a) || !Number.isFinite(b)) { | |||
throw new ArgumentOutOfRangeError('a and b must be finite numbers'); | |||
} | |||
export const bindBook: BindFunction = async (options: BindFunctionOptions): Promise<Buffer> => { | |||
const { input, output, } = await v.parseAsync(optionsSchema, options); | |||
const selectedCompiler = AVAILABLE_COMPILERS.find((c) => c.name === input.sourceType); | |||
const selectedFormat = AVAILABLE_FORMATS.find((f) => f.name === output.format); | |||
assert(typeof selectedFormat !== 'undefined'); | |||
return a + b; | |||
return selectedFormat.bindBook(input); | |||
}; |
@@ -0,0 +1,97 @@ | |||
import { | |||
describe, | |||
it, | |||
expect, | |||
vi, | |||
beforeEach, | |||
Mock, afterEach, | |||
} from 'vitest'; | |||
import { readFile, readdir } from 'fs/promises'; | |||
import { compileFromInput } from '../../src/compilers/path'; | |||
vi.mock('fs/promises'); | |||
const completeBookFile = { | |||
title: 'Astro Sandbox', | |||
publisher: '', | |||
creator: 'John Doe', | |||
contributors: [ | |||
'Jane Doe' | |||
], | |||
description: 'Retrieve from package.json', | |||
subjects: [ | |||
'A subject of the publication', | |||
'Another subject of the publication' | |||
], | |||
rights: '© copyright notice or get from package.json LICENSE' | |||
}; | |||
describe('path compiler', () => { | |||
let mockReaddir: Mock; | |||
beforeEach(() => { | |||
mockReaddir = readdir as Mock; | |||
}); | |||
afterEach(() => { | |||
mockReaddir.mockReset(); | |||
}); | |||
let mockReadFile: Mock; | |||
beforeEach(() => { | |||
mockReadFile = readFile as Mock; | |||
}); | |||
afterEach(() => { | |||
mockReadFile.mockReset(); | |||
}); | |||
it('gets a list of file buffers', async () => { | |||
mockReaddir.mockResolvedValue([ | |||
'patchouli.book.json', | |||
'patchouli.binding.json', | |||
]); | |||
mockReadFile.mockImplementation(async (path: string) => { | |||
if (path.endsWith('/patchouli.book.json')) { | |||
return JSON.stringify(completeBookFile); | |||
} | |||
if (path.endsWith('/patchouli.binding.json')) { | |||
return JSON.stringify({ | |||
}); | |||
} | |||
return ''; | |||
}); | |||
await compileFromInput({ | |||
path: 'path/to/project', | |||
}); | |||
}); | |||
describe('astro', () => { | |||
it('reads the binding file', async () => { | |||
mockReaddir.mockResolvedValue([ | |||
'patchouli.book.json', | |||
'patchouli.binding.json', | |||
]); | |||
mockReadFile.mockImplementation(async (path: string) => { | |||
if (path.endsWith('/patchouli.book.json')) { | |||
return JSON.stringify(completeBookFile); | |||
} | |||
if (path.endsWith('/patchouli.binding.json')) { | |||
return JSON.stringify({ | |||
generatorDistDirectory: './custom-dir', | |||
}); | |||
} | |||
return ''; | |||
}); | |||
await compileFromInput({ | |||
path: 'path/to/project', | |||
}); | |||
}); | |||
}); | |||
}); |
@@ -1,16 +1,16 @@ | |||
import { describe, it, expect } from 'vitest'; | |||
import { add } from '../src'; | |||
import { bindBook } from '../src'; | |||
describe('core', () => { | |||
describe.skip('core', () => { | |||
it('returns result', () => { | |||
expect(add({ a: 1, b: 1 })).toEqual(2); | |||
expect(bindBook({ a: 1, b: 1 })).toEqual(2); | |||
}); | |||
it('throws when given invalid argument types', () => { | |||
expect(() => add({ a: '1' as unknown as number, b: 1 })).toThrow(TypeError); | |||
expect(() => bindBook({ a: '1' as unknown as number, b: 1 })).toThrow(TypeError); | |||
}); | |||
it('throws when given out-of-range arguments', () => { | |||
expect(() => add({ a: Infinity, b: 1 })).toThrow(RangeError); | |||
expect(() => bindBook({ a: Infinity, b: 1 })).toThrow(RangeError); | |||
}); | |||
}); |
@@ -0,0 +1,24 @@ | |||
# build output | |||
dist/ | |||
# generated types | |||
.astro/ | |||
# dependencies | |||
node_modules/ | |||
# logs | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
pnpm-debug.log* | |||
# environment variables | |||
.env | |||
.env.production | |||
# macOS-specific files | |||
.DS_Store | |||
# jetbrains setting folder | |||
.idea/ |
@@ -0,0 +1,47 @@ | |||
# Astro Starter Kit: Minimal | |||
```sh | |||
npm create astro@latest -- --template minimal | |||
``` | |||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) | |||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) | |||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) | |||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun! | |||
## 🚀 Project Structure | |||
Inside of your Astro project, you'll see the following folders and files: | |||
```text | |||
/ | |||
├── public/ | |||
├── src/ | |||
│ └── pages/ | |||
│ └── index.astro | |||
└── package.json | |||
``` | |||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. | |||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. | |||
Any static assets, like images, can be placed in the `public/` directory. | |||
## 🧞 Commands | |||
All commands are run from the root of the project, from a terminal: | |||
| Command | Action | | |||
| :------------------------ | :----------------------------------------------- | | |||
| `npm install` | Installs dependencies | | |||
| `npm run dev` | Starts local dev server at `localhost:4321` | | |||
| `npm run build` | Build your production site to `./dist/` | | |||
| `npm run preview` | Preview your build locally, before deploying | | |||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | |||
| `npm run astro -- --help` | Get help using the Astro CLI | | |||
## 👀 Want to learn more? | |||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). |
@@ -0,0 +1,4 @@ | |||
import { defineConfig } from 'astro/config'; | |||
// https://astro.build/config | |||
export default defineConfig({}); |
@@ -0,0 +1,21 @@ | |||
{ | |||
"name": "@modal-sh/patchouli-sandbox-astro", | |||
"type": "module", | |||
"version": "0.0.1", | |||
"scripts": { | |||
"dev": "astro dev", | |||
"start": "astro dev", | |||
"build": "astro check && astro build && patchouli -t epub -o dist/book.epub", | |||
"preview": "astro preview", | |||
"bind": "node ./node_modules/@modal-sh/patchouli-cli/dist/cjs/development/index.js", | |||
"astro": "astro" | |||
}, | |||
"dependencies": { | |||
"astro": "^4.6.3", | |||
"@astrojs/check": "^0.5.10", | |||
"typescript": "^5.4.5" | |||
}, | |||
"devDependencies": { | |||
"@modal-sh/patchouli-cli": "workspace:*" | |||
} | |||
} |
@@ -0,0 +1,14 @@ | |||
{ | |||
"title": "Astro Sandbox", | |||
"publisher": "", | |||
"creator": "John Doe", | |||
"contributors": [ | |||
"Jane Doe" | |||
], | |||
"description": "Retrieve from package.json", | |||
"subjects": [ | |||
"A subject of the publication", | |||
"Another subject of the publication" | |||
], | |||
"rights": "© copyright notice or get from package.json LICENSE" | |||
} |
@@ -0,0 +1,9 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> | |||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> | |||
<style> | |||
path { fill: #000; } | |||
@media (prefers-color-scheme: dark) { | |||
path { fill: #FFF; } | |||
} | |||
</style> | |||
</svg> |
@@ -0,0 +1,29 @@ | |||
import { z, defineCollection } from 'astro:content'; | |||
const specialCollection = defineCollection({ | |||
type: 'content', | |||
schema: z.object({ | |||
title: z.string(), | |||
}), | |||
}); | |||
const chaptersCollection = defineCollection({ | |||
type: 'content', | |||
schema: z.object({ | |||
title: z.string(), | |||
}), | |||
}); | |||
const appendicesCollection = defineCollection({ | |||
type: 'content', | |||
schema: z.object({ | |||
title: z.string(), | |||
}), | |||
}); | |||
export const collections = { | |||
'special': specialCollection, | |||
'chapters': chaptersCollection, | |||
'appendices': appendicesCollection, | |||
}; | |||
@@ -0,0 +1 @@ | |||
/// <reference types="astro/client" /> |
@@ -0,0 +1,16 @@ | |||
--- | |||
--- | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | |||
<meta name="viewport" content="width=device-width" /> | |||
<meta name="generator" content={Astro.generator} /> | |||
<title>Astro</title> | |||
</head> | |||
<body> | |||
<h1>Astro</h1> | |||
</body> | |||
</html> |
@@ -0,0 +1,3 @@ | |||
{ | |||
"extends": "astro/tsconfigs/strict" | |||
} |
@@ -1,2 +1,2 @@ | |||
packages: | |||
- 'packages/*' | |||
- 'packages/**/*' |