Browse Source

Add compilers and extractors

Used for producing epubs.
master
TheoryOfNekomata 6 months ago
parent
commit
2c02cdf798
34 changed files with 3482 additions and 331 deletions
  1. +1
    -0
      packages/cli/package.json
  2. +19
    -14
      packages/cli/src/commands.ts
  3. +3
    -3
      packages/cli/src/index.ts
  4. +0
    -82
      packages/cli/src/modules/adder/adder.controller.ts
  5. +0
    -21
      packages/cli/src/modules/adder/adder.service.ts
  6. +0
    -2
      packages/cli/src/modules/adder/index.ts
  7. +55
    -0
      packages/cli/src/modules/bind/bind.controller.ts
  8. +37
    -0
      packages/cli/src/modules/bind/bind.service.ts
  9. +2
    -0
      packages/cli/src/modules/bind/index.ts
  10. +0
    -0
      packages/cli/src/modules/compiler/compiler.service.ts
  11. +0
    -0
      packages/cli/src/modules/compiler/index.ts
  12. +10
    -1
      packages/cli/src/packages/cli-wrapper.ts
  13. +4
    -4
      packages/cli/test/index.test.ts
  14. +6
    -0
      packages/core/package.json
  15. +28
    -0
      packages/core/src/common.ts
  16. +44
    -0
      packages/core/src/compilers/archive/index.ts
  17. +66
    -0
      packages/core/src/compilers/path/index.ts
  18. +7
    -0
      packages/core/src/formats/epub/index.ts
  19. +7
    -0
      packages/core/src/formats/pdf/index.ts
  20. +45
    -27
      packages/core/src/index.ts
  21. +97
    -0
      packages/core/test/compilers/path.test.ts
  22. +5
    -5
      packages/core/test/index.test.ts
  23. +24
    -0
      packages/sandboxes/astro/.gitignore
  24. +47
    -0
      packages/sandboxes/astro/README.md
  25. +4
    -0
      packages/sandboxes/astro/astro.config.mjs
  26. +21
    -0
      packages/sandboxes/astro/package.json
  27. +14
    -0
      packages/sandboxes/astro/patchouli.book.json
  28. +9
    -0
      packages/sandboxes/astro/public/favicon.svg
  29. +29
    -0
      packages/sandboxes/astro/src/content/config.ts
  30. +1
    -0
      packages/sandboxes/astro/src/env.d.ts
  31. +16
    -0
      packages/sandboxes/astro/src/pages/index.astro
  32. +3
    -0
      packages/sandboxes/astro/tsconfig.json
  33. +2877
    -171
      pnpm-lock.yaml
  34. +1
    -1
      pnpm-workspace.yaml

+ 1
- 0
packages/cli/package.json View File

@@ -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"
} }
} }

+ 19
- 14
packages/cli/src/commands.ts View File

@@ -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',
}
},
}); });
}; };

+ 3
- 3
packages/cli/src/index.ts View File

@@ -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);


+ 0
- 82
packages/cli/src/modules/adder/adder.controller.ts View File

@@ -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;
}
}

+ 0
- 21
packages/cli/src/modules/adder/adder.service.ts View File

@@ -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,
};

+ 0
- 2
packages/cli/src/modules/adder/index.ts View File

@@ -1,2 +0,0 @@
export * from './adder.controller';
export * from './adder.service';

+ 55
- 0
packages/cli/src/modules/bind/bind.controller.ts View File

@@ -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;
}
}

+ 37
- 0
packages/cli/src/modules/bind/bind.service.ts View File

@@ -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}.`;
}
}

+ 2
- 0
packages/cli/src/modules/bind/index.ts View File

@@ -0,0 +1,2 @@
export * from './bind.controller';
export * from './bind.service';

+ 0
- 0
packages/cli/src/modules/compiler/compiler.service.ts View File


+ 0
- 0
packages/cli/src/modules/compiler/index.ts View File


+ 10
- 1
packages/cli/src/packages/cli-wrapper.ts View File

@@ -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,


+ 4
- 4
packages/cli/test/index.test.ts View File

@@ -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');
}); });




+ 6
- 0
packages/core/package.json View File

@@ -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"
} }
} }

+ 28
- 0
packages/core/src/common.ts View File

@@ -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()),
});

+ 44
- 0
packages/core/src/compilers/archive/index.ts View File

@@ -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');
};

+ 66
- 0
packages/core/src/compilers/path/index.ts View File

@@ -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 [

];
};

+ 7
- 0
packages/core/src/formats/epub/index.ts View File

@@ -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);
};

+ 7
- 0
packages/core/src/formats/pdf/index.ts View File

@@ -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);
};

+ 45
- 27
packages/core/src/index.ts View File

@@ -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);
}; };

+ 97
- 0
packages/core/test/compilers/path.test.ts View File

@@ -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',
});
});
});
});

+ 5
- 5
packages/core/test/index.test.ts View File

@@ -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);
}); });
}); });

+ 24
- 0
packages/sandboxes/astro/.gitignore View File

@@ -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/

+ 47
- 0
packages/sandboxes/astro/README.md View File

@@ -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).

+ 4
- 0
packages/sandboxes/astro/astro.config.mjs View File

@@ -0,0 +1,4 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({});

+ 21
- 0
packages/sandboxes/astro/package.json View File

@@ -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:*"
}
}

+ 14
- 0
packages/sandboxes/astro/patchouli.book.json View File

@@ -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"
}

+ 9
- 0
packages/sandboxes/astro/public/favicon.svg View File

@@ -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>

+ 29
- 0
packages/sandboxes/astro/src/content/config.ts View File

@@ -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,
};


+ 1
- 0
packages/sandboxes/astro/src/env.d.ts View File

@@ -0,0 +1 @@
/// <reference types="astro/client" />

+ 16
- 0
packages/sandboxes/astro/src/pages/index.astro View File

@@ -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>

+ 3
- 0
packages/sandboxes/astro/tsconfig.json View File

@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}

+ 2877
- 171
pnpm-lock.yaml
File diff suppressed because it is too large
View File


+ 1
- 1
pnpm-workspace.yaml View File

@@ -1,2 +1,2 @@
packages: packages:
- 'packages/*'
- 'packages/**/*'

Loading…
Cancel
Save