@@ -66,6 +66,7 @@ | |||||
"@clack/prompts": "^0.7.0", | "@clack/prompts": "^0.7.0", | ||||
"@modal-sh/patchouli-core": "workspace:*", | "@modal-sh/patchouli-core": "workspace:*", | ||||
"pino": "^8.20.0", | "pino": "^8.20.0", | ||||
"valibot": "^0.30.0", | |||||
"yargs": "^17.7.2" | "yargs": "^17.7.2" | ||||
} | } | ||||
} | } |
@@ -1,22 +1,27 @@ | |||||
import { Cli } from './packages/cli-wrapper'; | import { Cli } from './packages/cli-wrapper'; | ||||
import { AdderController, AdderControllerImpl } from './modules/adder'; | |||||
import { BindController, BindControllerImpl } from './modules/bind'; | |||||
export const addCommands = (cli: Cli) => { | export const addCommands = (cli: Cli) => { | ||||
const adderController: AdderController = new AdderControllerImpl(); | |||||
const bindController: BindController = new BindControllerImpl(); | |||||
return cli | return cli | ||||
.command({ | .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'; | import { hideBin } from 'yargs/helpers'; | ||||
const cli = createCli({ | const cli = createCli({ | ||||
name: 'cli', | |||||
name: 'patchouli', | |||||
}); | }); | ||||
addCommands(cli); | 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; | describe: string; | ||||
handler: CommandHandler; | handler: CommandHandler; | ||||
interactiveMode?: boolean; | interactiveMode?: boolean; | ||||
options?: Record<string, { | |||||
alias?: string, | |||||
describe: string, | |||||
type: string, | |||||
default?: unknown, | |||||
hidden?: boolean, | |||||
}>; | |||||
} | } | ||||
type PromptValueArgs = [Record<string, string>, ...never[]] | type PromptValueArgs = [Record<string, string>, ...never[]] | ||||
@@ -265,7 +272,9 @@ export class Cli { | |||||
const commandArgs = { | const commandArgs = { | ||||
...args, | ...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'), | usage: parameters.map((p) => `${command} ${p}`).join('\n'), | ||||
handler: this.buildHandler(handler, { | handler: this.buildHandler(handler, { | ||||
option, | option, | ||||
@@ -1,7 +1,7 @@ | |||||
import { describe, it, expect, vi, beforeAll } from 'vitest'; | import { describe, it, expect, vi, beforeAll } from 'vitest'; | ||||
import { createCli } from '../src/cli'; | import { createCli } from '../src/cli'; | ||||
import { addCommands } from '../src/commands'; | 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'; | import { Cli } from '../src/packages/cli-wrapper'; | ||||
vi.mock('process'); | vi.mock('process'); | ||||
@@ -22,7 +22,7 @@ describe('cli', () => { | |||||
}); | }); | ||||
it('returns error when given invalid inputs', async () => { | 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'); | throw new InvalidArgumentTypeError('Invalid input'); | ||||
}); | }); | ||||
@@ -31,7 +31,7 @@ describe('cli', () => { | |||||
}); | }); | ||||
it('returns error when given out-of-range inputs', async () => { | 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'); | throw new ArgumentOutOfRangeError('Out of range'); | ||||
}); | }); | ||||
@@ -40,7 +40,7 @@ describe('cli', () => { | |||||
}); | }); | ||||
it('returns error when an unexpected error occurs', async () => { | 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'); | throw new Error('Unexpected error'); | ||||
}); | }); | ||||
@@ -8,6 +8,9 @@ | |||||
"engines": { | "engines": { | ||||
"node": ">=12" | "node": ">=12" | ||||
}, | }, | ||||
"bin": { | |||||
"patchouli": "./dist/cjs/production/index.js" | |||||
}, | |||||
"license": "UNLICENSED", | "license": "UNLICENSED", | ||||
"keywords": [ | "keywords": [ | ||||
"pridepack" | "pridepack" | ||||
@@ -60,5 +63,8 @@ | |||||
}, | }, | ||||
"typesVersions": { | "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 { describe, it, expect } from 'vitest'; | ||||
import { add } from '../src'; | |||||
import { bindBook } from '../src'; | |||||
describe('core', () => { | |||||
describe.skip('core', () => { | |||||
it('returns result', () => { | 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', () => { | 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', () => { | 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/*' | |||||
- 'packages/**/*' |