This commit is contained in:
Drew Powers 2021-05-03 12:26:10 -06:00 committed by GitHub
parent c93201a909
commit 94038d3297
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 2777 additions and 3003 deletions

View file

@ -6,8 +6,5 @@
"access": "public", "access": "public",
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"ignore": [ "ignore": ["@example/*", "www"]
"@example/*",
"www"
]
} }

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
**/dist/*

62
.vscode/launch.json vendored
View file

@ -1,36 +1,32 @@
// A launch configuration that compiles the extension and then opens it inside a new window // A launch configuration that compiles the extension and then opens it inside a new window
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"name": "Launch Client", "name": "Launch Client",
"runtimeExecutable": "${execPath}", "runtimeExecutable": "${execPath}",
"args": [ "args": ["--extensionDevelopmentPath=${workspaceRoot}/vscode"],
"--extensionDevelopmentPath=${workspaceRoot}/vscode" "outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"],
], "preLaunchTask": {
"outFiles": [ "type": "npm",
"${workspaceRoot}/vscode/dist/**/*.js" "script": "build:extension"
], }
"preLaunchTask": { },
"type": "npm", {
"script": "build:extension" "type": "node",
} "request": "attach",
}, "name": "Attach to Server",
{ "port": 6040,
"type": "node", "restart": true,
"request": "attach", "outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"]
"name": "Attach to Server", }
"port": 6040, ],
"restart": true, "compounds": [
"outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"] {
}, "name": "Launch Extension",
], "configurations": ["Launch Client", "Attach to Server"]
"compounds": [ }
{ ]
"name": "Launch Extension",
"configurations": ["Launch Client", "Attach to Server"]
}
]
} }

29
.vscode/tasks.json vendored
View file

@ -1,18 +1,15 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"type": "npm", "type": "npm",
"script": "build:extension", "script": "build:extension",
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
"reveal": "never" "reveal": "never"
}, },
"problemMatcher": [ "problemMatcher": ["$tsc"]
"$tsc" }
] ]
}
]
} }

View file

@ -43,7 +43,6 @@ npm run build
To deploy your Astro site to production, upload the contents of `/dist` to your favorite static site host. To deploy your Astro site to production, upload the contents of `/dist` to your favorite static site host.
## 🥾 Guides ## 🥾 Guides
### 🚀 Basic Usage ### 🚀 Basic Usage
@ -66,8 +65,8 @@ Even though nearly-everything [is configurable][docs-config], we recommend start
Routing happens in `src/pages/*`. Every `.astro` or `.md.astro` file in this folder corresponds with a public URL. For example: Routing happens in `src/pages/*`. Every `.astro` or `.md.astro` file in this folder corresponds with a public URL. For example:
| Local file | Public URL | | Local file | Public URL |
| :--------------------------------------- | :------------------------------ | | :------------------------------------- | :------------------------------ |
| `src/pages/index.astro` | `/index.html` | | `src/pages/index.astro` | `/index.html` |
| `src/pages/post/my-blog-post.md.astro` | `/post/my-blog-post/index.html` | | `src/pages/post/my-blog-post.md.astro` | `/post/my-blog-post/index.html` |
@ -164,10 +163,10 @@ Astro will automatically create a `/sitemap.xml` for you for SEO! Be sure to set
👉 [**Collections API**][docs-collections] 👉 [**Collections API**][docs-collections]
## ⚙️ Config ## ⚙️ Config
👉 [**`astro.config.mjs` Reference**][docs-config] 👉 [**`astro.config.mjs` Reference**][docs-config]
## 📚 API ## 📚 API
👉 [**Full API Reference**][docs-api] 👉 [**Full API Reference**][docs-api]

View file

@ -36,7 +36,7 @@ Runs the Astro development server. This starts an HTTP server that responds to r
See the [dev server](./dev.md) docs for more information on how the dev server works. See the [dev server](./dev.md) docs for more information on how the dev server works.
__Flags__ **Flags**
##### `--port` ##### `--port`

View file

@ -14,7 +14,7 @@ The dev server will serve the following special routes:
### /400 ### /400
This is a custom __400__ status code page. You can add this route by adding a page component to your `src/pages` folder: This is a custom **400** status code page. You can add this route by adding a page component to your `src/pages` folder:
``` ```
├── src/ ├── src/
@ -27,13 +27,10 @@ For any URL you visit that doesn't have a corresponding page, the `400.astro` fi
### /500 ### /500
This is a custom __500__ status code page. You can add this route by adding a page component to your `src/pages` folder: This is a custom **500** status code page. You can add this route by adding a page component to your `src/pages` folder:
```astro ```astro
├── src/ ├── src/ │ ├── components/ │ └── pages/ │ └── 500.astro
│ ├── components/
│ └── pages/
│ └── 500.astro
``` ```
This page is used any time an error occurs in the dev server. This page is used any time an error occurs in the dev server.

View file

@ -116,26 +116,25 @@ export let name;
`.astro` files can end up looking very similar to `.jsx` files, but there are a few key differences. Here's a comparison between the two formats. `.astro` files can end up looking very similar to `.jsx` files, but there are a few key differences. Here's a comparison between the two formats.
| Feature | Astro | JSX | | Feature | Astro | JSX |
|------------------------- |------------------------------------------ |---------------------------------------------------- | | ---------------------------- | ---------------------------------------- | -------------------------------------------------- |
| File extension | `.astro` | `.jsx` or `.tsx` | | File extension | `.astro` | `.jsx` or `.tsx` |
| User-Defined Components | `<Capitalized>` | `<Capitalized>` | | User-Defined Components | `<Capitalized>` | `<Capitalized>` |
| Expression Syntax | `{}` | `{}` | | Expression Syntax | `{}` | `{}` |
| Spread Attributes | `{...props}` | `{...props}` | | Spread Attributes | `{...props}` | `{...props}` |
| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` | | Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` |
| Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` | | Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` |
| IDE Support | WIP - [VS Code][code-ext] | Phenomenal | | IDE Support | WIP - [VS Code][code-ext] | Phenomenal |
| Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope | | Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope |
| Fragments | Automatic | Wrap with `<Fragment>` or `<>` | | Fragments | Automatic | Wrap with `<Fragment>` or `<>` |
| Multiple frameworks per-file | Yes | No | | Multiple frameworks per-file | Yes | No |
| Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) | | Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) |
| Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` | | Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` |
| Special Characters | `&nbsp;` | `{'\xa0'}` or `{String.fromCharCode(160)}` | | Special Characters | `&nbsp;` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
| Attributes | `dash-case` | `camelCase` | | Attributes | `dash-case` | `camelCase` |
### TODO: Styling ### TODO: Styling
### TODO: Composition (Slots) ### TODO: Composition (Slots)
[code-ext]: https://marketplace.visualstudio.com/items?itemName=astro-build.astro [code-ext]: https://marketplace.visualstudio.com/items?itemName=astro-build.astro

View file

@ -3,18 +3,18 @@ import { useState } from 'preact/hooks';
/** a counter written in Preact */ /** a counter written in Preact */
export default function PreactCounter({ children }) { export default function PreactCounter({ children }) {
const [count, setCount] = useState(0) const [count, setCount] = useState(0);
const add = () => setCount(i => i + 1); const add = () => setCount((i) => i + 1);
const subtract = () => setCount(i => i - 1); const subtract = () => setCount((i) => i - 1);
return <> return (
<div className="counter"> <>
<div className="counter">
<button onClick={subtract}>-</button> <button onClick={subtract}>-</button>
<pre>{count}</pre> <pre>{count}</pre>
<button onClick={add}>+</button> <button onClick={add}>+</button>
</div> </div>
<div className="children"> <div className="children">{children}</div>
{children} </>
</div> );
</>
} }

View file

@ -2,18 +2,18 @@ import React, { useState } from 'react';
/** a counter written in React */ /** a counter written in React */
export default function ReactCounter({ children }) { export default function ReactCounter({ children }) {
const [count, setCount] = useState(0) const [count, setCount] = useState(0);
const add = () => setCount(i => i + 1); const add = () => setCount((i) => i + 1);
const subtract = () => setCount(i => i - 1); const subtract = () => setCount((i) => i - 1);
return <> return (
<div className="counter"> <>
<div className="counter">
<button onClick={subtract}>-</button> <button onClick={subtract}>-</button>
<pre>{count}</pre> <pre>{count}</pre>
<button onClick={add}>+</button> <button onClick={add}>+</button>
</div> </div>
<div className="children"> <div className="children">{children}</div>
{children} </>
</div> );
</>
} }

View file

@ -1,21 +1,21 @@
# Astro Demo # Astro Demo
## Getting setup ## Getting set up
1. Checkout Astro at: https://github.com/snowpackjs/astro 1. Check out Astro at: https://github.com/snowpackjs/astro
1. Install and build Astro: 1. Install and build Astro:
```shell ```shell
npm install npm install
npm run build npm run build
``` ```
2. Link Astro: 2. Link Astro:
```shell ```shell
npm link npm link
``` ```
2. In this project link Astro and install other deps: 2. In this project link Astro and install other deps:

View file

@ -1,4 +1,4 @@
import {h} from 'preact'; import { h } from 'preact';
export default function CompanyLogo({ user }) { export default function CompanyLogo({ user }) {
return ( return (

View file

@ -117,5 +117,9 @@ function PluginSearchPageLive() {
} }
export default function PluginSearchPage(props) { export default function PluginSearchPage(props) {
return import.meta.env.astro ? <div>Loading...</div> : <PluginSearchPageLive {...props} /> return import.meta.env.astro ? (
<div>Loading...</div>
) : (
<PluginSearchPageLive {...props} />
);
} }

View file

@ -1,17 +1,20 @@
import docsearch from 'docsearch.js/dist/cdn/docsearch.min.js'; import docsearch from 'docsearch.js/dist/cdn/docsearch.min.js';
customElements.define('doc-search', class extends HTMLElement { customElements.define(
connectedCallback() { 'doc-search',
if(!this._setup) { class extends HTMLElement {
const apiKey = this.getAttribute('api-key'); connectedCallback() {
const selector = this.getAttribute('selector'); if (!this._setup) {
docsearch({ const apiKey = this.getAttribute('api-key');
apiKey: apiKey, const selector = this.getAttribute('selector');
indexName: 'snowpack', docsearch({
inputSelector: selector, apiKey: apiKey,
debug: true // Set debug to true if you want to inspect the dropdown indexName: 'snowpack',
}); inputSelector: selector,
this._setup = true; debug: true, // Set debug to true if you want to inspect the dropdown
});
this._setup = true;
}
} }
} },
}); );

View file

@ -1,2 +1,4 @@
const snowpackManifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../../snowpack/package.json'), 'utf8')); const snowpackManifest = JSON.parse(
fs.readFileSync(path.join(__dirname, '../../snowpack/package.json'), 'utf8'),
);
export default snowpackManifest.version; export default snowpackManifest.version;

View file

@ -1,8 +1,5 @@
{ {
"ignoreChanges": [ "ignoreChanges": ["**/test/**", "**/*.md"],
"**/test/**",
"**/*.md"
],
"useWorkspaces": true, "useWorkspaces": true,
"version": "4.0.0" "version": "4.0.0"
} }

View file

@ -5,11 +5,11 @@
"release": "yarn build && yarn changeset publish", "release": "yarn build && yarn changeset publish",
"build": "yarn build:core", "build": "yarn build:core",
"build:core": "lerna run build --scope astro --scope astro-parser --scope create-astro", "build:core": "lerna run build --scope astro --scope astro-parser --scope create-astro",
"format": "prettier -w '**/*.{js,jsx,ts,tsx,md,json}'",
"lint": "eslint 'packages/**/*.ts'", "lint": "eslint 'packages/**/*.ts'",
"test": "yarn test:core && yarn test:prettier", "test": "yarn test:core && yarn test:prettier",
"test:core": "cd packages/astro && npm test", "test:core": "cd packages/astro && npm test",
"test:prettier": "cd tools/prettier-plugin-astro && npm test", "test:prettier": "cd tools/prettier-plugin-astro && npm test"
"format": "prettier -w '**/*.{js,jsx,ts,tsx,md,json}'"
}, },
"workspaces": [ "workspaces": [
"packages/*", "packages/*",

View file

@ -1,27 +1,27 @@
declare module '#astro/compiler' { declare module '#astro/compiler' {
export * from 'astro/dist/types/compiler'; export * from 'astro/dist/types/compiler';
} }
declare module '#astro/ast' { declare module '#astro/ast' {
export * from 'astro/dist/types/ast'; export * from 'astro/dist/types/ast';
} }
declare module '#astro/build' { declare module '#astro/build' {
export * from 'astro/dist/types/build'; export * from 'astro/dist/types/build';
} }
declare module '#astro/cli' { declare module '#astro/cli' {
export * from 'astro/dist/types/cli'; export * from 'astro/dist/types/cli';
} }
declare module '#astro/config' { declare module '#astro/config' {
export * from 'astro/dist/types/config'; export * from 'astro/dist/types/config';
} }
declare module '#astro/dev' { declare module '#astro/dev' {
export * from 'astro/dist/types/dev'; export * from 'astro/dist/types/dev';
} }
declare module '#astro/logger' { declare module '#astro/logger' {
export * from 'astro/dist/types/logger'; export * from 'astro/dist/types/logger';
} }
declare module '#astro/runtime' { declare module '#astro/runtime' {
export * from 'astro/dist/types/runtime'; export * from 'astro/dist/types/runtime';
} }
declare module '#astro/search' { declare module '#astro/search' {
export * from 'astro/dist/types/search'; export * from 'astro/dist/types/search';
} }

View file

@ -16,7 +16,6 @@ import { generateRSS } from './build/rss.js';
import { generateSitemap } from './build/sitemap.js'; import { generateSitemap } from './build/sitemap.js';
import { collectStatics } from './build/static.js'; import { collectStatics } from './build/static.js';
import { canonicalURL } from './build/util.js'; import { canonicalURL } from './build/util.js';
import { pathToFileURL } from 'node:url';
const { mkdir, readFile, writeFile } = fsPromises; const { mkdir, readFile, writeFile } = fsPromises;
@ -69,8 +68,8 @@ async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf8'
interface WriteResultOptions { interface WriteResultOptions {
srcPath: string; srcPath: string;
result: LoadResult; result: LoadResult;
outPath: URL, outPath: URL;
encoding: null|'utf8' encoding: null | 'utf8';
} }
/** Utility for writing a build result to disk */ /** Utility for writing a build result to disk */

View file

@ -31,10 +31,10 @@ interface CLIState {
/** Determine which action the user requested */ /** Determine which action the user requested */
function resolveArgs(flags: Arguments): CLIState { function resolveArgs(flags: Arguments): CLIState {
const options: CLIState['options'] = { const options: CLIState['options'] = {
projectRoot: typeof flags.projectRoot === 'string' ? flags.projectRoot: undefined, projectRoot: typeof flags.projectRoot === 'string' ? flags.projectRoot : undefined,
sitemap: typeof flags.sitemap === 'boolean' ? flags.sitemap : undefined, sitemap: typeof flags.sitemap === 'boolean' ? flags.sitemap : undefined,
port: typeof flags.port === 'number' ? flags.port : undefined, port: typeof flags.port === 'number' ? flags.port : undefined,
config: typeof flags.config === 'string' ? flags.config : undefined config: typeof flags.config === 'string' ? flags.config : undefined,
}; };
if (flags.version) { if (flags.version) {

View file

@ -15,7 +15,7 @@ import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
import { transform } from './transform/index.js'; import { transform } from './transform/index.js';
import { codegen } from './codegen/index.js'; import { codegen } from './codegen/index.js';
export { scopeRule } from './transform/postcss-scoped-styles/index.js' export { scopeRule } from './transform/postcss-scoped-styles/index.js';
/** Return Astro internal import URL */ /** Return Astro internal import URL */
function internalImport(internalPath: string) { function internalImport(internalPath: string) {

View file

@ -26,8 +26,8 @@ function validateConfig(config: any): void {
} }
} }
if(typeof config.devOptions?.port !== 'number') { if (typeof config.devOptions?.port !== 'number') {
throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`) throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`);
} }
} }

View file

@ -75,7 +75,7 @@ export default async function dev(astroConfig: AstroConfig) {
res.statusCode = 500; res.statusCode = 500;
let errorResult = await runtime.load(`/500?error=${encodeURIComponent(result.error.stack || result.error.toString())}`); let errorResult = await runtime.load(`/500?error=${encodeURIComponent(result.error.stack || result.error.toString())}`);
if(errorResult.statusCode === 200) { if (errorResult.statusCode === 200) {
if (errorResult.contentType) { if (errorResult.contentType) {
res.setHeader('Content-Type', errorResult.contentType); res.setHeader('Content-Type', errorResult.contentType);
} }

View file

@ -49,6 +49,6 @@ export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRe
}; };
return tree.map((subtree) => { return tree.map((subtree) => {
if (subtree.type === 'text') return JSON.stringify(subtree.value); if (subtree.type === 'text') return JSON.stringify(subtree.value);
return toH(innerH, subtree).__SERIALIZED return toH(innerH, subtree).__SERIALIZED;
}); });
}); });

View file

@ -16,7 +16,7 @@ export const defaultLogDestination = new Writable({
dest = process.stdout; dest = process.stdout;
} }
let type = event.type; let type = event.type;
if(type !== null) { if (type !== null) {
if (event.level === 'info') { if (event.level === 'info') {
type = bold(blue(type)); type = bold(blue(type));
} else if (event.level === 'error') { } else if (event.level === 'error') {
@ -135,10 +135,10 @@ export const logger = {
}; };
// For silencing libraries that go directly to console.warn // For silencing libraries that go directly to console.warn
export function trapWarn(cb: (...args: any[]) => void = () =>{}) { export function trapWarn(cb: (...args: any[]) => void = () => {}) {
const warn = console.warn; const warn = console.warn;
console.warn = function(...args: any[]) { console.warn = function (...args: any[]) {
cb(...args); cb(...args);
}; };
return () => console.warn = warn; return () => (console.warn = warn);
} }

View file

@ -200,14 +200,14 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
// For first release query params are not passed to components. // For first release query params are not passed to components.
// An exception is made for dev server specific routes. // An exception is made for dev server specific routes.
if(reqPath !== '/500') { if (reqPath !== '/500') {
requestURL.search = ''; requestURL.search = '';
} }
let html = (await mod.exports.__renderPage({ let html = (await mod.exports.__renderPage({
request: { request: {
// params should go here when implemented // params should go here when implemented
url: requestURL url: requestURL,
}, },
children: [], children: [],
props: { collection }, props: { collection },

View file

@ -92,14 +92,14 @@ export function searchForPage(url: URL, astroRoot: URL): SearchResult {
} }
} }
if(reqPath === '/500') { if (reqPath === '/500') {
return { return {
statusCode: 200, statusCode: 200,
location: { location: {
fileURL: new URL('./frontend/500.astro', import.meta.url), fileURL: new URL('./frontend/500.astro', import.meta.url),
snowpackURL: `/_astro_internal/500.astro.js` snowpackURL: `/_astro_internal/500.astro.js`,
}, },
pathname: reqPath pathname: reqPath,
}; };
} }

View file

@ -4,7 +4,6 @@ import * as assert from 'uvu/assert';
import { loadConfig } from '#astro/config'; import { loadConfig } from '#astro/config';
import { createRuntime } from '#astro/runtime'; import { createRuntime } from '#astro/runtime';
const DType = suite('doctype'); const DType = suite('doctype');
let runtime, setupError; let runtime, setupError;

View file

@ -12,7 +12,7 @@ ConfigPath('can be passed via --config', async (context) => {
process.stdout.setEncoding('utf8'); process.stdout.setEncoding('utf8');
for await (const chunk of process.stdout) { for await (const chunk of process.stdout) {
if(/Server started/.test(chunk)) { if (/Server started/.test(chunk)) {
break; break;
} }
} }

View file

@ -17,7 +17,7 @@ ConfigPort('can be specified via --port flag', async (context) => {
process.stdout.setEncoding('utf8'); process.stdout.setEncoding('utf8');
for await (const chunk of process.stdout) { for await (const chunk of process.stdout) {
if(/Local:/.test(chunk)) { if (/Local:/.test(chunk)) {
assert.ok(/:3002/.test(chunk), 'Using the right port'); assert.ok(/:3002/.test(chunk), 'Using the right port');
break; break;
} }

View file

@ -1,5 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function PreactComponent({ children }) { export default function PreactComponent({ children }) {
return <div id="preact">{children}</div> return <div id="preact">{children}</div>;
} }

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
export default function() { export default function () {
return ( return (
<div> <div>
<button type="button">Increment -</button> <button type="button">Increment -</button>
</div> </div>
) );
} }

View file

@ -1,5 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function({ name }) { export default function ({ name }) {
return <div id={name}>{name}</div> return <div id={name}>{name}</div>;
} }

View file

@ -1,7 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function(props) { export default function (props) {
return ( return <div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div>;
<div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div> }
);
};

View file

@ -1,5 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function() { export default function () {
return <div id="test">Testing</div> return <div id="test">Testing</div>;
} }

View file

@ -1,5 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function({ name }) { export default function ({ name }) {
return <div id="test">Hello {name}</div> return <div id="test">Hello {name}</div>;
} }

View file

@ -4,4 +4,3 @@ import cheerio from 'cheerio';
export function doc(html) { export function doc(html) {
return cheerio.load(html); return cheerio.load(html);
} }

View file

@ -1,6 +1,7 @@
# create-astro # create-astro
## 0.1.0 ## 0.1.0
### Minor Changes ### Minor Changes
- ed63132: Added **interactive mode** with a fesh new UI. - ed63132: Added **interactive mode** with a fesh new UI.

View file

@ -1,12 +1,15 @@
# create-astro # create-astro
## Scaffolding for Astro projects ## Scaffolding for Astro projects
**With NPM:** **With NPM:**
```bash ```bash
npm init astro npm init astro
``` ```
**With Yarn:** **With Yarn:**
```bash ```bash
yarn create astro yarn create astro
``` ```

View file

@ -1,4 +1,4 @@
import React, {FC, useEffect} from 'react'; import React, { FC, useEffect } from 'react';
import { prepareTemplate, isEmpty, emptyDir } from '../utils'; import { prepareTemplate, isEmpty, emptyDir } from '../utils';
import Header from './Header'; import Header from './Header';
import Install from './Install'; import Install from './Install';
@ -8,86 +8,91 @@ import Confirm from './Confirm';
import Finalize from './Finalize'; import Finalize from './Finalize';
interface Context { interface Context {
use: 'npm'|'yarn'; use: 'npm' | 'yarn';
run: boolean; run: boolean;
projectExists?: boolean; projectExists?: boolean;
force?: boolean; force?: boolean;
projectName?: string; projectName?: string;
template?: string; template?: string;
templates: string[]; templates: string[];
ready?: boolean; ready?: boolean;
} }
const getStep = ({ projectName, projectExists: exists, template, force, ready }: Context) => { const getStep = ({ projectName, projectExists: exists, template, force, ready }: Context) => {
switch (true) { switch (true) {
case !projectName: return { case !projectName:
key: 'projectName', return {
Component: ProjectName key: 'projectName',
}; Component: ProjectName,
case projectName && exists === true && typeof force === 'undefined': return { };
key: 'force', case projectName && exists === true && typeof force === 'undefined':
Component: Confirm return {
} key: 'force',
case (exists === false || force) && !template: return { Component: Confirm,
key: 'template', };
Component: Template case (exists === false || force) && !template:
}; return {
case !ready: return { key: 'template',
key: 'install', Component: Template,
Component: Install };
}; case !ready:
default: return { return {
key: 'final', key: 'install',
Component: Finalize Component: Install,
} };
} default:
} return {
key: 'final',
Component: Finalize,
};
}
};
const App: FC<{ context: Context }> = ({ context }) => { const App: FC<{ context: Context }> = ({ context }) => {
const [state, setState] = React.useState(context); const [state, setState] = React.useState(context);
const step = React.useRef(getStep(context)); const step = React.useRef(getStep(context));
const onSubmit = (value: string|boolean) => { const onSubmit = (value: string | boolean) => {
const { key } = step.current; const { key } = step.current;
const newState = { ...state, [key]: value }; const newState = { ...state, [key]: value };
step.current = getStep(newState) step.current = getStep(newState);
setState(newState) setState(newState);
} };
useEffect(() => { useEffect(() => {
let isSubscribed = true let isSubscribed = true;
if (state.projectName && typeof state.projectExists === 'undefined') { if (state.projectName && typeof state.projectExists === 'undefined') {
const newState = { ...state, projectExists: !isEmpty(state.projectName) }; const newState = { ...state, projectExists: !isEmpty(state.projectName) };
step.current = getStep(newState) step.current = getStep(newState);
if (isSubscribed) { if (isSubscribed) {
setState(newState); setState(newState);
} }
} }
if (state.projectName && (state.projectExists === false || state.force) && state.template) { if (state.projectName && (state.projectExists === false || state.force) && state.template) {
if (state.force) emptyDir(state.projectName); if (state.force) emptyDir(state.projectName);
prepareTemplate(context.use, state.template, state.projectName).then(() => { prepareTemplate(context.use, state.template, state.projectName).then(() => {
if (isSubscribed) { if (isSubscribed) {
setState(v => { setState((v) => {
const newState = {...v, ready: true }; const newState = { ...v, ready: true };
step.current = getStep(newState); step.current = getStep(newState);
return newState; return newState;
}); });
} }
}); });
} }
return () => { return () => {
isSubscribed = false; isSubscribed = false;
} };
}, [state]); }, [state]);
const { Component } = step.current; const { Component } = step.current;
return ( return (
<> <>
<Header context={state}/> <Header context={state} />
<Component context={state} onSubmit={onSubmit} /> <Component context={state} onSubmit={onSubmit} />
</> </>
) );
}; };
export default App; export default App;

View file

@ -31,7 +31,7 @@ const Confirm: FC<{ message?: any; context: any; onSubmit: (value: boolean) => v
items={[ items={[
{ {
value: false, value: false,
label: 'no' label: 'no',
}, },
{ {
value: true, value: true,

View file

@ -2,4 +2,4 @@ import React from 'react';
import { Text } from 'ink'; import { Text } from 'ink';
import { isWin } from '../utils'; import { isWin } from '../utils';
export default ({ children }) => isWin() ? null : <Text>{children}</Text> export default ({ children }) => (isWin() ? null : <Text>{children}</Text>);

View file

@ -2,8 +2,11 @@ import React, { FC } from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { isDone } from '../utils'; import { isDone } from '../utils';
const Exit: FC<{ didError?: boolean }> = ({ didError }) => isDone ? null : <Box marginTop={1} display="flex"> const Exit: FC<{ didError?: boolean }> = ({ didError }) =>
<Text color={didError ? "#FF1639" : "#FFBE2D"}>[abort]</Text> isDone ? null : (
<Text> astro cancelled</Text> <Box marginTop={1} display="flex">
</Box> <Text color={didError ? '#FF1639' : '#FFBE2D'}>[abort]</Text>
<Text> astro cancelled</Text>
</Box>
);
export default Exit; export default Exit;

View file

@ -8,20 +8,25 @@ const Finalize: FC<{ context: any }> = ({ context: { use, projectName } }) => {
process.exit(0); process.exit(0);
}, []); }, []);
return <> return (
<>
<Box display="flex"> <Box display="flex">
<Text color="#17C083">{'[ yes ]'}</Text> <Text color="#17C083">{'[ yes ]'}</Text>
<Text> Project initialized at <Text color="#3894FF">./{projectName}</Text></Text> <Text>
{' '}
Project initialized at <Text color="#3894FF">./{projectName}</Text>
</Text>
</Box> </Box>
<Box display="flex" marginY={1}> <Box display="flex" marginY={1}>
<Text dimColor>{'[ tip ]'}</Text> <Text dimColor>{'[ tip ]'}</Text>
<Box display="flex" marginLeft={1} flexDirection="column"> <Box display="flex" marginLeft={1} flexDirection="column">
<Text>Get started by running</Text> <Text>Get started by running</Text>
<Text color="#3894FF">cd ./{projectName}</Text> <Text color="#3894FF">cd ./{projectName}</Text>
<Text color="#3894FF">{use} start</Text> <Text color="#3894FF">{use} start</Text>
</Box> </Box>
</Box> </Box>
</>; </>
);
}; };
export default Finalize; export default Finalize;

View file

@ -2,19 +2,27 @@ import React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
const getMessage = ({ projectName, template }) => { const getMessage = ({ projectName, template }) => {
switch (true) { switch (true) {
case !projectName: return <Text dimColor>Gathering mission details</Text>; case !projectName:
case !template: return <Text dimColor>Optimizing navigational system</Text>; return <Text dimColor>Gathering mission details</Text>;
default: return <Text color="black" backgroundColor="white"> {projectName} </Text> case !template:
} return <Text dimColor>Optimizing navigational system</Text>;
} default:
return (
<Text color="black" backgroundColor="white">
{' '}
{projectName}{' '}
</Text>
);
}
};
const Header: React.FC<{ context: any }> = ({ context }) => ( const Header: React.FC<{ context: any }> = ({ context }) => (
<Box width={48} display="flex" marginY={1}> <Box width={48} display="flex" marginY={1}>
<Text backgroundColor="#882DE7" color="white">{' astro '}</Text> <Text backgroundColor="#882DE7" color="white">
<Box marginLeft={1}> {' astro '}
{getMessage(context)} </Text>
</Box> <Box marginLeft={1}>{getMessage(context)}</Box>
</Box> </Box>
) );
export default Header; export default Header;

View file

@ -2,61 +2,79 @@ import React, { FC } from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { ARGS, ARG } from '../config'; import { ARGS, ARG } from '../config';
const Type: FC<{ type: any, enum?: string[] }> = ({ type, enum: e }) => { const Type: FC<{ type: any; enum?: string[] }> = ({ type, enum: e }) => {
if (type === Boolean) { if (type === Boolean) {
return <>
<Text color="#3894FF">true</Text>
<Text dimColor>|</Text>
<Text color="#3894FF">false</Text>
</>
}
if (e?.length > 0) {
return <>
{e.map((item, i, { length: len}) => {
if (i !== len - 1) {
return <Box key={item}>
<Text color="#17C083">{item}</Text>
<Text dimColor>|</Text>
</Box>
}
return <Text color="#17C083" key={item}>{item}</Text>
})}
</>
}
return <Text color="#3894FF">string</Text>;
}
const Command: FC<{ name: string, info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => {
return ( return (
<Box display="flex" alignItems="flex-start"> <>
<Box width={24} display="flex" flexGrow={0}> <Text color="#3894FF">true</Text>
<Text color="whiteBright">--{name}</Text>{alias && <Text dimColor> -{alias}</Text>} <Text dimColor>|</Text>
</Box> <Text color="#3894FF">false</Text>
<Box width={24}> </>
<Type type={type} enum={e} />
</Box>
<Box>
<Text>{description}</Text>
</Box>
</Box>
); );
} }
if (e?.length > 0) {
const Help: FC<{ context: any }> = ({ context: { templates }}) => {
return ( return (
<> <>
<Box width={48} display="flex" marginY={1}> {e.map((item, i, { length: len }) => {
<Text backgroundColor="#882DE7" color="white">{' astro '}</Text> if (i !== len - 1) {
<Box marginLeft={1}> return (
<Text color="black" backgroundColor="white"> help </Text> <Box key={item}>
</Box> <Text color="#17C083">{item}</Text>
</Box> <Text dimColor>|</Text>
<Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column"> </Box>
{Object.entries(ARGS).map(([name, info]) => <Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} /> )} );
</Box> }
</>
) return (
<Text color="#17C083" key={item}>
{item}
</Text>
);
})}
</>
);
}
return <Text color="#3894FF">string</Text>;
};
const Command: FC<{ name: string; info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => {
return (
<Box display="flex" alignItems="flex-start">
<Box width={24} display="flex" flexGrow={0}>
<Text color="whiteBright">--{name}</Text>
{alias && <Text dimColor> -{alias}</Text>}
</Box>
<Box width={24}>
<Type type={type} enum={e} />
</Box>
<Box>
<Text>{description}</Text>
</Box>
</Box>
);
};
const Help: FC<{ context: any }> = ({ context: { templates } }) => {
return (
<>
<Box width={48} display="flex" marginY={1}>
<Text backgroundColor="#882DE7" color="white">
{' astro '}
</Text>
<Box marginLeft={1}>
<Text color="black" backgroundColor="white">
{' '}
help{' '}
</Text>
</Box>
</Box>
<Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column">
{Object.entries(ARGS).map(([name, info]) => (
<Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} />
))}
</Box>
</>
);
}; };
export default Help; export default Help;

View file

@ -4,16 +4,20 @@ import Spacer from './Spacer';
import Spinner from './Spinner'; import Spinner from './Spinner';
const Install: FC<{ context: any }> = ({ context: { use } }) => { const Install: FC<{ context: any }> = ({ context: { use } }) => {
return <> return (
<>
<Box display="flex"> <Box display="flex">
<Spinner/> <Spinner />
<Text> Initiating launch sequence...</Text> <Text> Initiating launch sequence...</Text>
</Box> </Box>
<Box> <Box>
<Spacer /> <Spacer />
<Text color="white" dimColor>(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)</Text> <Text color="white" dimColor>
(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)
</Text>
</Box> </Box>
</>; </>
);
}; };
export default Install; export default Install;

View file

@ -9,7 +9,8 @@ const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) =>
const [value, setValue] = React.useState(''); const [value, setValue] = React.useState('');
const handleSubmit = (v: string) => onSubmit(v); const handleSubmit = (v: string) => onSubmit(v);
return <> return (
<>
<Box display="flex"> <Box display="flex">
<Text color="#17C083">{'[query]'}</Text> <Text color="#17C083">{'[query]'}</Text>
<Text> What is your project name?</Text> <Text> What is your project name?</Text>
@ -18,7 +19,8 @@ const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) =>
<Spacer /> <Spacer />
<Input value={value} onChange={setValue} onSubmit={handleSubmit} placeholder="my-project" /> <Input value={value} onChange={setValue} onSubmit={handleSubmit} placeholder="my-project" />
</Box> </Box>
</>; </>
);
}; };
export default ProjectName; export default ProjectName;

View file

@ -5,28 +5,28 @@ import { Text, Box } from 'ink';
const { default: Select } = SelectInput; const { default: Select } = SelectInput;
interface Props { interface Props {
isSelected?: boolean; isSelected?: boolean;
label: string; label: string;
description?: string; description?: string;
} }
const Indicator: FC<Props> = ({ isSelected }) => isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text> const Indicator: FC<Props> = ({ isSelected }) => (isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text>);
const Item: FC<Props> = ({isSelected = false, label, description }) => ( const Item: FC<Props> = ({ isSelected = false, label, description }) => (
<Box display="flex"> <Box display="flex">
<Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>{label}</Text> <Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>
{isSelected && description && typeof description === 'string' && <Text> {description}</Text>} {label}
{isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>} </Text>
</Box> {isSelected && description && typeof description === 'string' && <Text> {description}</Text>}
{isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>}
</Box>
); );
interface SelectProps { interface SelectProps {
items: { value: string|number|boolean, label: string, description?: any }[] items: { value: string | number | boolean; label: string; description?: any }[];
onSelect(value: string|number|boolean): void; onSelect(value: string | number | boolean): void;
} }
const CustomSelect: FC<SelectProps> = ({ items, onSelect }) => { const CustomSelect: FC<SelectProps> = ({ items, onSelect }) => {
const handleSelect = ({ value }) => onSelect(value); const handleSelect = ({ value }) => onSelect(value);
return ( return <Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} />;
<Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} /> };
)
}
export default CustomSelect; export default CustomSelect;

View file

@ -1,5 +1,5 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Box } from 'ink'; import { Box } from 'ink';
const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} /> const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} />;
export default Spacer; export default Spacer;

View file

@ -6,14 +6,14 @@ const Spinner: FC<{ type?: keyof typeof spinners }> = ({ type = 'countdown' }) =
const [i, setI] = useState(0); const [i, setI] = useState(0);
useEffect(() => { useEffect(() => {
const _ = setInterval(() => { const _ = setInterval(() => {
setI(v => (v < frames.length - 1) ? v + 1 : 0) setI((v) => (v < frames.length - 1 ? v + 1 : 0));
}, interval) }, interval);
return () => clearInterval(_); return () => clearInterval(_);
}, []) }, []);
return frames[i] return frames[i];
} };
const spinners = { const spinners = {
countdown: { countdown: {
@ -35,73 +35,73 @@ const spinners = {
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
@ -122,79 +122,79 @@ const spinners = {
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083">{' '}</Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7"> </Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
<Text backgroundColor="#17C083">{' '}</Text> <Text backgroundColor="#17C083"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
<Text backgroundColor="#23B1AF">{' '}</Text> <Text backgroundColor="#23B1AF"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
<Text backgroundColor="#2CA5D2">{' '}</Text> <Text backgroundColor="#2CA5D2"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
<Text backgroundColor="#3894FF">{' '}</Text> <Text backgroundColor="#3894FF"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
<Text backgroundColor="#5076F9">{' '}</Text> <Text backgroundColor="#5076F9"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
<Text backgroundColor="#6858F1">{' '}</Text> <Text backgroundColor="#6858F1"> </Text>
</Box>, </Box>,
<Box display="flex"> <Box display="flex">
<Text backgroundColor="#882DE7">{' '}</Text> <Text backgroundColor="#882DE7">{' '}</Text>
</Box>, </Box>,
] ],
} },
} };
export default Spinner; export default Spinner;

View file

@ -3,7 +3,7 @@ import { Box, Text } from 'ink';
import Spacer from './Spacer'; import Spacer from './Spacer';
import Select from './Select'; import Select from './Select';
const Template: FC<{ context: any, onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => { const Template: FC<{ context: any; onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => {
const items = templates.map(({ title: label, ...rest }) => ({ ...rest, label })); const items = templates.map(({ title: label, ...rest }) => ({ ...rest, label }));
return ( return (

View file

@ -1,49 +1,49 @@
import type * as arg from 'arg'; import type * as arg from 'arg';
export interface ARG { export interface ARG {
type: any; type: any;
description: string; description: string;
enum?: string[]; enum?: string[];
alias?: string; alias?: string;
} }
export const ARGS: Record<string, ARG> = { export const ARGS: Record<string, ARG> = {
'template': { template: {
type: String, type: String,
description: 'specifies template to use' description: 'specifies template to use',
}, },
'use': { use: {
type: String, type: String,
enum: ['npm', 'yarn'], enum: ['npm', 'yarn'],
description: 'specifies package manager to use' description: 'specifies package manager to use',
}, },
'run': { run: {
type: Boolean, type: Boolean,
description: 'should dependencies be installed automatically?' description: 'should dependencies be installed automatically?',
}, },
'force': { force: {
type: Boolean, type: Boolean,
alias: 'f', alias: 'f',
description: 'should existing files be overwritten?' description: 'should existing files be overwritten?',
}, },
'version': { version: {
type: Boolean, type: Boolean,
alias: 'v', alias: 'v',
description: 'prints current version' description: 'prints current version',
}, },
'help': { help: {
type: Boolean, type: Boolean,
alias: 'h', alias: 'h',
description: 'prints this message' description: 'prints this message',
} },
} };
export const args = Object.entries(ARGS).reduce((acc, [name, info]) => { export const args = Object.entries(ARGS).reduce((acc, [name, info]) => {
const key = `--${name}`; const key = `--${name}`;
const spec = { ...acc, [key]: info.type }; const spec = { ...acc, [key]: info.type };
if (info.alias) { if (info.alias) {
spec[`-${info.alias}`] = key; spec[`-${info.alias}`] = key;
} }
return spec return spec;
}, {} as arg.Spec); }, {} as arg.Spec);

View file

@ -3,7 +3,7 @@ import React from 'react';
import App from './components/App'; import App from './components/App';
import Version from './components/Version'; import Version from './components/Version';
import Exit from './components/Exit'; import Exit from './components/Exit';
import {render} from 'ink'; import { render } from 'ink';
import { getTemplates, addProcessListeners } from './utils'; import { getTemplates, addProcessListeners } from './utils';
import { args as argsConfig } from './config'; import { args as argsConfig } from './config';
import arg from 'arg'; import arg from 'arg';
@ -11,36 +11,36 @@ import Help from './components/Help';
/** main `create-astro` CLI */ /** main `create-astro` CLI */
export default async function createAstro() { export default async function createAstro() {
const args = arg(argsConfig); const args = arg(argsConfig);
const projectName = args._[0]; const projectName = args._[0];
if (args['--version']) { if (args['--version']) {
return render(<Version />); return render(<Version />);
} }
const templates = await getTemplates(); const templates = await getTemplates();
if (args['--help']) { if (args['--help']) {
return render(<Help context={{ templates }} />) return render(<Help context={{ templates }} />);
} }
const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm'; const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm';
const use = (args['--use'] ?? pkgManager) as 'npm'|'yarn'; const use = (args['--use'] ?? pkgManager) as 'npm' | 'yarn';
const template = args['--template']; const template = args['--template'];
const force = args['--force']; const force = args['--force'];
const run = args['--run'] ?? true; const run = args['--run'] ?? true;
const app = render(<App context={{ projectName, template, templates, force, run, use }} />); const app = render(<App context={{ projectName, template, templates, force, run, use }} />);
const onError = () => { const onError = () => {
if (app) app.clear(); if (app) app.clear();
render(<Exit didError />); render(<Exit didError />);
} };
const onExit = () => { const onExit = () => {
if (app) app.clear(); if (app) app.clear();
render(<Exit />); render(<Exit />);
} };
addProcessListeners([ addProcessListeners([
['uncaughtException', onError], ['uncaughtException', onError],
['exit', onExit], ['exit', onExit],
['SIGINT', onExit], ['SIGINT', onExit],
['SIGTERM', onExit], ['SIGTERM', onExit],
]) ]);
} }

View file

@ -1,4 +1,4 @@
{ {
"title": "Blank", "title": "Blank",
"description": "a bare-bones, ultra-minimal template" "description": "a bare-bones, ultra-minimal template"
} }

View file

@ -21,10 +21,8 @@ Inside of your Astro project, you'll see the following folders and files:
Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. Astro looks for `.astro` or `.md.astro` 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. 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. Any static assets, like images, can be placed in the `public/` directory.
## 👀 Want to learn more? ## 👀 Want to learn more?

View file

@ -1,5 +1,5 @@
{ {
"title": "Getting Started", "title": "Getting Started",
"description": "a friendly starting point for new astronauts", "description": "a friendly starting point for new astronauts",
"rank": 999 "rank": 999
} }

View file

@ -6,8 +6,8 @@ import decompress from 'decompress';
const listeners = new Map(); const listeners = new Map();
export async function addProcessListeners(handlers: [NodeJS.Signals|string, NodeJS.SignalsListener][]) { export async function addProcessListeners(handlers: [NodeJS.Signals | string, NodeJS.SignalsListener][]) {
for (const [event,handler] of handlers) { for (const [event, handler] of handlers) {
listeners.set(event, handler); listeners.set(event, handler);
process.once(event as NodeJS.Signals, handler); process.once(event as NodeJS.Signals, handler);
} }
@ -21,18 +21,20 @@ export async function cancelProcessListeners() {
} }
export async function getTemplates() { export async function getTemplates() {
const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url)); const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url));
const templateFiles = await fs.readdir(templatesRoot, 'utf8'); const templateFiles = await fs.readdir(templatesRoot, 'utf8');
const templates = templateFiles.filter(t => t.endsWith('.tgz')); const templates = templateFiles.filter((t) => t.endsWith('.tgz'));
const metafile = templateFiles.find(t => t.endsWith('meta.json')); const metafile = templateFiles.find((t) => t.endsWith('meta.json'));
const meta = await fs.readFile(resolve(templatesRoot, metafile)).then(r => JSON.parse(r.toString())); const meta = await fs.readFile(resolve(templatesRoot, metafile)).then((r) => JSON.parse(r.toString()));
return templates.map(template => { return templates
.map((template) => {
const value = basename(template, '.tgz'); const value = basename(template, '.tgz');
if (meta[value]) return { ...meta[value], value }; if (meta[value]) return { ...meta[value], value };
return { value }; return { value };
}).sort((a, b) => { })
.sort((a, b) => {
const aRank = a.rank ?? 0; const aRank = a.rank ?? 0;
const bRank = b.rank ?? 0; const bRank = b.rank ?? 0;
if (aRank > bRank) return -1; if (aRank > bRank) return -1;
@ -49,28 +51,29 @@ export async function rewriteFiles(projectName: string) {
const tasks = []; const tasks = [];
tasks.push(fs.rename(resolve(dest, '_gitignore'), resolve(dest, '.gitignore'))); tasks.push(fs.rename(resolve(dest, '_gitignore'), resolve(dest, '.gitignore')));
tasks.push( tasks.push(
fs.readFile(resolve(dest, 'package.json')) fs
.then(res => JSON.parse(res.toString())) .readFile(resolve(dest, 'package.json'))
.then(json => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2)) .then((res) => JSON.parse(res.toString()))
.then(res => fs.writeFile(resolve(dest, 'package.json'), res)) .then((json) => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2))
.then((res) => fs.writeFile(resolve(dest, 'package.json'), res))
); );
return Promise.all(tasks); return Promise.all(tasks);
} }
export async function prepareTemplate(use: 'npm'|'yarn', name: string, dest: string) { export async function prepareTemplate(use: 'npm' | 'yarn', name: string, dest: string) {
const projectName = dest; const projectName = dest;
dest = resolve(dest); dest = resolve(dest);
const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url)); const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url));
await decompress(template, dest); await decompress(template, dest);
await rewriteFiles(projectName); await rewriteFiles(projectName);
try { try {
await run(use, use === 'npm' ? 'i' : null, dest); await run(use, use === 'npm' ? 'i' : null, dest);
} catch (e) { } catch (e) {
cleanup(true); cleanup(true);
} }
isDone = true; isDone = true;
return; return;
} }
export function cleanup(didError = false) { export function cleanup(didError = false) {
@ -81,10 +84,10 @@ export function cleanup(didError = false) {
} }
export function killChildren() { export function killChildren() {
childrenProcesses.forEach(p => p.kill('SIGINT')); childrenProcesses.forEach((p) => p.kill('SIGINT'));
} }
export function run(pkgManager: 'npm'|'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> { export function run(pkgManager: 'npm' | 'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const p = spawn(pkgManager, command ? [command] : [], { const p = spawn(pkgManager, command ? [command] : [], {
shell: true, shell: true,
@ -102,40 +105,40 @@ export function isWin() {
} }
export function isEmpty(path) { export function isEmpty(path) {
try { try {
const files = readdirSync(resolve(path)); const files = readdirSync(resolve(path));
if (files.length > 0) { if (files.length > 0) {
return false; return false;
} else { } else {
return true; return true;
}
} catch (err) {
if (err.code !== 'ENOENT') throw err;
} }
return true; } catch (err) {
if (err.code !== 'ENOENT') throw err;
}
return true;
} }
export function emptyDir(dir) { export function emptyDir(dir) {
dir = resolve(dir); dir = resolve(dir);
if (!existsSync(dir)) { if (!existsSync(dir)) {
return return;
} }
for (const file of readdirSync(dir)) { for (const file of readdirSync(dir)) {
const abs = resolve(dir, file) const abs = resolve(dir, file);
if (lstatSync(abs).isDirectory()) { if (lstatSync(abs).isDirectory()) {
emptyDir(abs) emptyDir(abs);
rmdirSync(abs) rmdirSync(abs);
} else { } else {
unlinkSync(abs) unlinkSync(abs);
} }
} }
} }
export function getValidPackageName(projectName: string) { export function getValidPackageName(projectName: string) {
const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/ const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
if (packageNameRegExp.test(projectName)) { if (packageNameRegExp.test(projectName)) {
return projectName return projectName;
} }
return projectName return projectName

View file

@ -1,16 +1,16 @@
{ {
"name": "astro-scripts", "name": "astro-scripts",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./index.js", "main": "./index.js",
"bin": { "bin": {
"astro-scripts": "./index.js" "astro-scripts": "./index.js"
}, },
"dependencies": { "dependencies": {
"arg": "^5.0.0", "arg": "^5.0.0",
"esbuild": "^0.11.16", "esbuild": "^0.11.16",
"globby": "^11.0.3", "globby": "^11.0.3",
"tar": "^6.1.0" "tar": "^6.1.0"
} }
} }

View file

@ -4,57 +4,58 @@ import { relative, isAbsolute, join, dirname } from 'path';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const convertMessage = ({ message, start, end, filename, frame }) => ({ const convertMessage = ({ message, start, end, filename, frame }) => ({
text: message, text: message,
location: start && end && { location: start &&
file: filename, end && {
line: start.line, file: filename,
column: start.column, line: start.line,
length: start.line === end.line ? end.column - start.column : 0, column: start.column,
lineText: frame, length: start.line === end.line ? end.column - start.column : 0,
lineText: frame,
}, },
}) });
const handleLoad = async (args, generate) => { const handleLoad = async (args, generate) => {
const { path } = args; const { path } = args;
const source = await fs.readFile(path, 'utf8'); const source = await fs.readFile(path, 'utf8');
const filename = relative(process.cwd(), path) const filename = relative(process.cwd(), path);
try { try {
let compileOptions = { css: false, generate, hydratable: true }; let compileOptions = { css: false, generate, hydratable: true };
let { js, warnings } = compile(source, { ...compileOptions, filename }) let { js, warnings } = compile(source, { ...compileOptions, filename });
let contents = js.code + `\n//# sourceMappingURL=` + js.map.toUrl() let contents = js.code + `\n//# sourceMappingURL=` + js.map.toUrl();
return { loader: 'js', contents, resolveDir: dirname(path), warnings: warnings.map(w => convertMessage(w)) }; return { loader: 'js', contents, resolveDir: dirname(path), warnings: warnings.map((w) => convertMessage(w)) };
} catch (e) { } catch (e) {
return { errors: [convertMessage(e)] } return { errors: [convertMessage(e)] };
} }
} };
export default function sveltePlugin() { export default function sveltePlugin() {
return { return {
name: 'svelte-esbuild', name: 'svelte-esbuild',
setup(build) { setup(build) {
build.onResolve({ filter: /\.svelte$/ }, args => { build.onResolve({ filter: /\.svelte$/ }, (args) => {
let path = args.path.replace(/\.(?:client|server)/, ''); let path = args.path.replace(/\.(?:client|server)/, '');
path = isAbsolute(path) ? path : join(args.resolveDir, path) path = isAbsolute(path) ? path : join(args.resolveDir, path);
if (/\.client\.svelte$/.test(args.path)) { if (/\.client\.svelte$/.test(args.path)) {
return { return {
path, path,
namespace: 'svelte:client', namespace: 'svelte:client',
} };
} }
if (/\.server\.svelte$/.test(args.path)) { if (/\.server\.svelte$/.test(args.path)) {
return { return {
path, path,
namespace: 'svelte:server', namespace: 'svelte:server',
} };
} }
}); });
build.onLoad({ filter: /.*/, namespace: 'svelte:client' }, (args) => handleLoad(args, 'dom')) build.onLoad({ filter: /.*/, namespace: 'svelte:client' }, (args) => handleLoad(args, 'dom'));
build.onLoad({ filter: /.*/, namespace: 'svelte:server' }, (args) => handleLoad(args, 'ssr')) build.onLoad({ filter: /.*/, namespace: 'svelte:server' }, (args) => handleLoad(args, 'ssr'));
}, },
} };
} }

View file

@ -42,7 +42,7 @@ module.exports.parsers = {
return node.end; return node.end;
}, },
astFormat: 'astro-expression', astFormat: 'astro-expression',
} },
}; };
const findExpressionsInAST = (node, collect = []) => { const findExpressionsInAST = (node, collect = []) => {
@ -50,24 +50,24 @@ const findExpressionsInAST = (node, collect = []) => {
return collect.concat(node); return collect.concat(node);
} }
if (node.children) { if (node.children) {
collect.push(...[].concat(...node.children.map(child => findExpressionsInAST(child)))); collect.push(...[].concat(...node.children.map((child) => findExpressionsInAST(child))));
} }
return collect; return collect;
} };
const formatExpression = ({ expression: { codeChunks, children }}, text, options) => { const formatExpression = ({ expression: { codeChunks, children } }, text, options) => {
if (children.length === 0) { if (children.length === 0) {
const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk. const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk.
if (codeStart && [`'`, `"`].includes(codeStart[0])) { if (codeStart && [`'`, `"`].includes(codeStart[0])) {
return `<script $ lang="ts">${codeChunks.join('')}</script>` return `<script $ lang="ts">${codeChunks.join('')}</script>`;
} }
return `{${codeChunks.join('')}}`; return `{${codeChunks.join('')}}`;
} }
return `<script $ lang="ts">${text}</script>`; return `<script $ lang="ts">${text}</script>`;
} };
const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find(v => v === '$'); const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find((v) => v === '$');
const walkDoc = (doc) => { const walkDoc = (doc) => {
let inAstroScript = false; let inAstroScript = false;
@ -77,38 +77,38 @@ const walkDoc = (doc) => {
inAstroScript = true; inAstroScript = true;
parent.contents = { type: 'concat', parts: ['{'] }; parent.contents = { type: 'concat', parts: ['{'] };
} }
return node.parts.map(part => recurse(part, { parent: node })); return node.parts.map((part) => recurse(part, { parent: node }));
} }
if (inAstroScript) { if (inAstroScript) {
if (node.type === 'break-parent') { if (node.type === 'break-parent') {
parent.parts = parent.parts.filter(part => !['break-parent', 'line'].includes(part.type)); parent.parts = parent.parts.filter((part) => !['break-parent', 'line'].includes(part.type));
} }
if (node.type === 'indent') { if (node.type === 'indent') {
parent.parts = parent.parts.map(part => { parent.parts = parent.parts.map((part) => {
if (part.type !== 'indent') return part; if (part.type !== 'indent') return part;
return { return {
type: 'concat', type: 'concat',
parts: [part.contents] parts: [part.contents],
} };
}) });
} }
if (typeof node === 'string' && node.endsWith(';')) { if (typeof node === 'string' && node.endsWith(';')) {
parent.parts = parent.parts.map(part => { parent.parts = parent.parts.map((part) => {
if (typeof part === 'string' && part.endsWith(';')) return part.slice(0, -1); if (typeof part === 'string' && part.endsWith(';')) return part.slice(0, -1);
return part; return part;
}); });
} }
if (node === '</script>') { if (node === '</script>') {
parent.parts = parent.parts.map(part => part === '</script>' ? '}' : part); parent.parts = parent.parts.map((part) => (part === '</script>' ? '}' : part));
inAstroScript = false; inAstroScript = false;
} }
} }
if (['group', 'indent'].includes(node.type)) { if (['group', 'indent'].includes(node.type)) {
return recurse(node.contents, { parent: node }); return recurse(node.contents, { parent: node });
} }
} };
recurse(doc, { parent: null }); recurse(doc, { parent: null });
} };
/** @type {Record<string, import('prettier').Printer>} */ /** @type {Record<string, import('prettier').Printer>} */
module.exports.printers = { module.exports.printers = {
@ -129,18 +129,20 @@ module.exports.printers = {
if (node.type === 'Fragment' && node.isRoot) { if (node.type === 'Fragment' && node.isRoot) {
const expressions = findExpressionsInAST(node); const expressions = findExpressionsInAST(node);
if (expressions.length > 0) { if (expressions.length > 0) {
const parts = [].concat(...expressions.map((expr, i, all) => { const parts = [].concat(
const prev = all[i - 1]; ...expressions.map((expr, i, all) => {
const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start); const prev = all[i - 1];
const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options); const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start);
const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options);
if (i === all.length - 1) { if (i === all.length - 1) {
const end = node.text.slice(expr.end - node.start); const end = node.text.slice(expr.end - node.start);
return [start, exprText, end] return [start, exprText, end];
} }
return [start, exprText] return [start, exprText];
})); })
);
const html = parts.join('\n'); const html = parts.join('\n');
const doc = textToDoc(html, { parser: 'html' }); const doc = textToDoc(html, { parser: 'html' });
walkDoc(doc); walkDoc(doc);

View file

@ -2,10 +2,10 @@ import { suite } from 'uvu';
import * as assert from 'uvu/assert'; import * as assert from 'uvu/assert';
import { format } from './test-utils.js'; import { format } from './test-utils.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url';
const Prettier = suite('Prettier formatting'); const Prettier = suite('Prettier formatting');
const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then(res => res.toString()) const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then((res) => res.toString());
/** /**
* Utility to get `[src, out]` files * Utility to get `[src, out]` files

View file

@ -1,3 +1,3 @@
{ {
"type": "module" "type": "module"
} }

View file

@ -1,36 +1,36 @@
{ {
"comments": { "comments": {
"blockComment": [ "<!--", "-->" ] "blockComment": ["<!--", "-->"]
}, },
"brackets": [ "brackets": [
["---", "---"], ["---", "---"],
["<!--", "-->"], ["<!--", "-->"],
["<", ">"], ["<", ">"],
["{", "}"], ["{", "}"],
["(", ")"] ["(", ")"]
], ],
"autoClosingPairs": [ "autoClosingPairs": [
{ "open": "{", "close": "}"}, { "open": "{", "close": "}" },
{ "open": "[", "close": "]"}, { "open": "[", "close": "]" },
{ "open": "(", "close": ")" }, { "open": "(", "close": ")" },
{ "open": "'", "close": "'" }, { "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" }, { "open": "\"", "close": "\"" },
{ "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]}, { "open": "<!--", "close": "-->", "notIn": ["comment", "string"] },
{ "open": "/**", "close": " */", "notIn": ["string"] } { "open": "/**", "close": " */", "notIn": ["string"] }
], ],
"autoCloseBefore": ";:.,=}])>` \n\t", "autoCloseBefore": ";:.,=}])>` \n\t",
"surroundingPairs": [ "surroundingPairs": [
{ "open": "'", "close": "'" }, { "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" }, { "open": "\"", "close": "\"" },
{ "open": "{", "close": "}"}, { "open": "{", "close": "}" },
{ "open": "[", "close": "]"}, { "open": "[", "close": "]" },
{ "open": "(", "close": ")" }, { "open": "(", "close": ")" },
{ "open": "<", "close": ">" } { "open": "<", "close": ">" }
], ],
"folding": { "folding": {
"markers": { "markers": {
"start": "^\\s*<!--\\s*#region\\b.*-->", "start": "^\\s*<!--\\s*#region\\b.*-->",
"end": "^\\s*<!--\\s*#endregion\\b.*-->" "end": "^\\s*<!--\\s*#endregion\\b.*-->"
} }
} }
} }

View file

@ -1,96 +1,96 @@
{ {
"name": "@astro.build/vscode", "name": "@astro.build/vscode",
"displayName": "Astro", "displayName": "Astro",
"description": "Language support for Astro", "description": "Language support for Astro",
"icon": "assets/icon.png", "icon": "assets/icon.png",
"galleryBanner": { "galleryBanner": {
"color": "#FF5D01", "color": "#FF5D01",
"theme": "dark" "theme": "dark"
}, },
"version": "0.3.0", "version": "0.3.0",
"author": "Astro", "author": "Astro",
"publisher": "astro-build", "publisher": "astro-build",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"vscode:prepublish": "npm run build", "vscode:prepublish": "npm run build",
"bootstrap": "cd packages/client && yarn && cd ../server && yarn", "bootstrap": "cd packages/client && yarn && cd ../server && yarn",
"build": "node scripts/build.mjs", "build": "node scripts/build.mjs",
"watch": "node scripts/watch.mjs" "watch": "node scripts/watch.mjs"
}, },
"engines": { "engines": {
"vscode": "^1.52.0" "vscode": "^1.52.0"
}, },
"activationEvents": [ "activationEvents": [
"onLanguage:astro" "onLanguage:astro"
], ],
"dependencies": { "dependencies": {
"vscode-html-languageservice": "^3.0.3", "vscode-html-languageservice": "^3.0.3",
"vscode-emmet-helper": "2.1.2" "vscode-emmet-helper": "2.1.2"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "0.10.0", "esbuild": "0.10.0",
"@astro-vscode/client": "file:./packages/client", "@astro-vscode/client": "file:./packages/client",
"@astro-vscode/server": "file:./packages/server" "@astro-vscode/server": "file:./packages/server"
}, },
"main": "./dist/index.js", "main": "./dist/index.js",
"files": [ "files": [
"dist/", "dist/",
"languages/", "languages/",
"syntaxes/" "syntaxes/"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"directory": "vscode", "directory": "vscode",
"url": "https://github.com/snowpackjs/astro" "url": "https://github.com/snowpackjs/astro"
}, },
"contributes": { "contributes": {
"configuration": { "configuration": {
"type": "object", "type": "object",
"title": "Astro configuration", "title": "Astro configuration",
"properties": { "properties": {
"astro.trace.server": { "astro.trace.server": {
"scope": "window", "scope": "window",
"type": "string", "type": "string",
"enum": [ "enum": [
"off", "off",
"messages", "messages",
"verbose" "verbose"
], ],
"default": "off", "default": "off",
"description": "Traces the communication between VS Code and the language server." "description": "Traces the communication between VS Code and the language server."
} }
} }
}, },
"languages": [ "languages": [
{ {
"id": "astro", "id": "astro",
"extensions": [ "extensions": [
".astro" ".astro"
], ],
"aliases": [ "aliases": [
"Astro" "Astro"
], ],
"configuration": "./languages/astro-language-configuration.json" "configuration": "./languages/astro-language-configuration.json"
} }
], ],
"grammars": [ "grammars": [
{ {
"language": "astro", "language": "astro",
"scopeName": "text.html.astro", "scopeName": "text.html.astro",
"path": "./syntaxes/astro.tmLanguage.json", "path": "./syntaxes/astro.tmLanguage.json",
"injectTo": [ "injectTo": [
"text.html.markdown" "text.html.markdown"
], ],
"embeddedLanguages": { "embeddedLanguages": {
"text.html.astro": "astro", "text.html.astro": "astro",
"text.html": "html", "text.html": "html",
"source.css": "css", "source.css": "css",
"source.scss": "scss", "source.scss": "scss",
"source.sass": "sass", "source.sass": "sass",
"source.tsx": "typescriptreact", "source.tsx": "typescriptreact",
"meta.embedded.block.frontmatter": "typescriptreact" "meta.embedded.block.frontmatter": "typescriptreact"
} }
} }
] ]
} }
} }

View file

@ -1,12 +1,12 @@
{ {
"name": "@astro-vscode/client", "name": "@astro-vscode/client",
"version": "0.1.0", "version": "0.1.0",
"author": "Skypack", "author": "Skypack",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"vscode-languageclient": "next" "vscode-languageclient": "next"
}, },
"devDependencies": { "devDependencies": {
"@types/vscode": "latest" "@types/vscode": "latest"
} }
} }

View file

@ -12,97 +12,78 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
/** */ /** */
export function activateTagClosing( export function activateTagClosing(
tagProvider: (document: TextDocument, position: Position) => Thenable<string>, tagProvider: (document: TextDocument, position: Position) => Thenable<string>,
supportedLanguages: { [id: string]: boolean }, supportedLanguages: { [id: string]: boolean },
configName: string configName: string
): Disposable { ): Disposable {
const disposables: Disposable[] = []; const disposables: Disposable[] = [];
workspace.onDidChangeTextDocument( workspace.onDidChangeTextDocument((event) => onDidChangeTextDocument(event.document, event.contentChanges), null, disposables);
(event) => onDidChangeTextDocument(event.document, event.contentChanges),
null,
disposables
);
let isEnabled = false; let isEnabled = false;
updateEnabledState(); updateEnabledState();
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
let timeout: NodeJS.Timer | undefined = void 0; let timeout: NodeJS.Timer | undefined = void 0;
/** Check if this feature is enabled */ /** Check if this feature is enabled */
function updateEnabledState() { function updateEnabledState() {
isEnabled = false; isEnabled = false;
const editor = window.activeTextEditor; const editor = window.activeTextEditor;
if (!editor) { if (!editor) {
return; return;
}
const document = editor.document;
if (!supportedLanguages[document.languageId]) {
return;
}
if (!workspace.getConfiguration(void 0, document.uri).get<boolean>(configName)) {
return;
}
isEnabled = true;
} }
const document = editor.document;
/** Handle text document changes */ if (!supportedLanguages[document.languageId]) {
function onDidChangeTextDocument( return;
document: TextDocument,
changes: readonly TextDocumentContentChangeEvent[]
) {
if (!isEnabled) {
return;
}
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
if (typeof timeout !== 'undefined') {
clearTimeout(timeout);
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (
('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) ||
(lastCharacter !== '>' && lastCharacter !== '/')
) {
return;
}
const rangeStart =
'range' in lastChange
? lastChange.range.start
: new Position(0, document.getText().length);
const version = document.version;
timeout = setTimeout(() => {
const position = new Position(
rangeStart.line,
rangeStart.character + lastChange.text.length
);
tagProvider(document, position).then((text) => {
if (text && isEnabled) {
const activeEditor = window.activeTextEditor;
if (activeEditor) {
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
const selections = activeEditor.selections;
if (
selections.length &&
selections.some((s) => s.active.isEqual(position))
) {
activeEditor.insertSnippet(
new SnippetString(text),
selections.map((s) => s.active)
);
} else {
activeEditor.insertSnippet(new SnippetString(text), position);
}
}
}
}
});
timeout = void 0;
}, 100);
} }
return Disposable.from(...disposables); if (!workspace.getConfiguration(void 0, document.uri).get<boolean>(configName)) {
return;
}
isEnabled = true;
}
/** Handle text document changes */
function onDidChangeTextDocument(document: TextDocument, changes: readonly TextDocumentContentChangeEvent[]) {
if (!isEnabled) {
return;
}
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
if (typeof timeout !== 'undefined') {
clearTimeout(timeout);
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) || (lastCharacter !== '>' && lastCharacter !== '/')) {
return;
}
const rangeStart = 'range' in lastChange ? lastChange.range.start : new Position(0, document.getText().length);
const version = document.version;
timeout = setTimeout(() => {
const position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
tagProvider(document, position).then((text) => {
if (text && isEnabled) {
const activeEditor = window.activeTextEditor;
if (activeEditor) {
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
const selections = activeEditor.selections;
if (selections.length && selections.some((s) => s.active.isEqual(position))) {
activeEditor.insertSnippet(
new SnippetString(text),
selections.map((s) => s.active)
);
} else {
activeEditor.insertSnippet(new SnippetString(text), position);
}
}
}
}
});
timeout = void 0;
}, 100);
}
return Disposable.from(...disposables);
} }

View file

@ -1,12 +1,10 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"], "exclude": ["node_modules"],
"references": [ "references": [{ "path": "../server" }]
{ "path": "../server" }
]
} }

View file

@ -1,13 +1,13 @@
import { VSCodeEmmetConfig } from 'vscode-emmet-helper'; import { VSCodeEmmetConfig } from 'vscode-emmet-helper';
export class ConfigManager { export class ConfigManager {
private emmetConfig: VSCodeEmmetConfig = {}; private emmetConfig: VSCodeEmmetConfig = {};
updateEmmetConfig(config: VSCodeEmmetConfig): void { updateEmmetConfig(config: VSCodeEmmetConfig): void {
this.emmetConfig = config || {}; this.emmetConfig = config || {};
} }
getEmmetConfig(): VSCodeEmmetConfig { getEmmetConfig(): VSCodeEmmetConfig {
return this.emmetConfig; return this.emmetConfig;
} }
} }

View file

@ -7,153 +7,147 @@ import { parseHtml } from './parseHtml';
import { parseAstro, AstroDocument } from './parseAstro'; import { parseAstro, AstroDocument } from './parseAstro';
export class Document implements TextDocument { export class Document implements TextDocument {
private content: string;
private content: string; languageId = 'astro';
version = 0;
html!: HTMLDocument;
astro!: AstroDocument;
languageId = 'astro'; constructor(public uri: string, text: string) {
version = 0; this.content = text;
html!: HTMLDocument; this.updateDocInfo();
astro!: AstroDocument; }
constructor(public uri: string, text: string) { private updateDocInfo() {
this.content = text; this.html = parseHtml(this.content);
this.updateDocInfo(); this.astro = parseAstro(this.content);
}
setText(text: string) {
this.content = text;
this.version++;
this.updateDocInfo();
}
/**
* Update the text between two positions.
* @param text The new text slice
* @param start Start offset of the new text
* @param end End offset of the new text
*/
update(text: string, start: number, end: number): void {
const content = this.getText();
this.setText(content.slice(0, start) + text + content.slice(end));
}
getText(): string {
return this.content;
}
/**
* Get the line and character based on the offset
* @param offset The index of the position
*/
positionAt(offset: number): Position {
offset = clamp(offset, 0, this.getTextLength());
const lineOffsets = this.getLineOffsets();
let low = 0;
let high = lineOffsets.length;
if (high === 0) {
return Position.create(0, offset);
} }
private updateDocInfo() { while (low < high) {
this.html = parseHtml(this.content); const mid = Math.floor((low + high) / 2);
this.astro = parseAstro(this.content); if (lineOffsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
}
} }
setText(text: string) { // low is the least x for which the line offset is larger than the current offset
this.content = text; // or array.length if no line offset is larger than the current offset
this.version++; const line = low - 1;
this.updateDocInfo(); return Position.create(line, offset - lineOffsets[line]);
}
/**
* Get the index of the line and character position
* @param position Line and character position
*/
offsetAt(position: Position): number {
const lineOffsets = this.getLineOffsets();
if (position.line >= lineOffsets.length) {
return this.getTextLength();
} else if (position.line < 0) {
return 0;
} }
/** const lineOffset = lineOffsets[position.line];
* Update the text between two positions. const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.getTextLength();
* @param text The new text slice
* @param start Start offset of the new text return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
* @param end End offset of the new text }
*/
update(text: string, start: number, end: number): void { getLineUntilOffset(offset: number): string {
const content = this.getText(); const { line, character } = this.positionAt(offset);
this.setText(content.slice(0, start) + text + content.slice(end)); return this.lines[line].slice(0, character);
}
private getLineOffsets() {
const lineOffsets = [];
const text = this.getText();
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
} }
getText(): string { if (isLineStart && text.length > 0) {
return this.content lineOffsets.push(text.length);
} }
/** return lineOffsets;
* Get the line and character based on the offset }
* @param offset The index of the position
*/
positionAt(offset: number): Position {
offset = clamp(offset, 0, this.getTextLength());
const lineOffsets = this.getLineOffsets(); /**
let low = 0; * Get the length of the document's content
let high = lineOffsets.length; */
if (high === 0) { getTextLength(): number {
return Position.create(0, offset); return this.getText().length;
} }
while (low < high) { /**
const mid = Math.floor((low + high) / 2); * Returns the file path if the url scheme is file
if (lineOffsets[mid] > offset) { */
high = mid; getFilePath(): string | null {
} else { return urlToPath(this.uri);
low = mid + 1; }
}
}
// low is the least x for which the line offset is larger than the current offset /**
// or array.length if no line offset is larger than the current offset * Get URL file path.
const line = low - 1; */
return Position.create(line, offset - lineOffsets[line]); getURL() {
} return this.uri;
}
/** get lines(): string[] {
* Get the index of the line and character position return this.getText().split(/\r?\n/);
* @param position Line and character position }
*/
offsetAt(position: Position): number {
const lineOffsets = this.getLineOffsets();
if (position.line >= lineOffsets.length) {
return this.getTextLength();
} else if (position.line < 0) {
return 0;
}
const lineOffset = lineOffsets[position.line];
const nextLineOffset =
position.line + 1 < lineOffsets.length
? lineOffsets[position.line + 1]
: this.getTextLength();
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
}
getLineUntilOffset(offset: number): string {
const { line, character } = this.positionAt(offset);
return this.lines[line].slice(0, character);
}
private getLineOffsets() {
const lineOffsets = [];
const text = this.getText();
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}
if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
}
/**
* Get the length of the document's content
*/
getTextLength(): number {
return this.getText().length;
}
/**
* Returns the file path if the url scheme is file
*/
getFilePath(): string | null {
return urlToPath(this.uri);
}
/**
* Get URL file path.
*/
getURL() {
return this.uri;
}
get lines(): string[] {
return this.getText().split(/\r?\n/);
}
get lineCount(): number {
return this.lines.length;
}
get lineCount(): number {
return this.lines.length;
}
} }

View file

@ -1,104 +1,94 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { import { TextDocumentContentChangeEvent, TextDocumentItem } from 'vscode-languageserver';
TextDocumentContentChangeEvent,
TextDocumentItem
} from 'vscode-languageserver';
import { Document } from './Document'; import { Document } from './Document';
import { normalizeUri } from '../../utils'; import { normalizeUri } from '../../utils';
export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose'; export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose';
export class DocumentManager { export class DocumentManager {
private emitter = new EventEmitter(); private emitter = new EventEmitter();
private openedInClient = new Set<string>(); private openedInClient = new Set<string>();
private documents: Map<string, Document> = new Map(); private documents: Map<string, Document> = new Map();
private locked = new Set<string>(); private locked = new Set<string>();
private deleteCandidates = new Set<string>(); private deleteCandidates = new Set<string>();
constructor( constructor(private createDocument: (textDocument: { uri: string; text: string }) => Document) {}
private createDocument: (textDocument: { uri: string, text: string }) => Document
) {}
get(uri: string) { get(uri: string) {
return this.documents.get(normalizeUri(uri)); return this.documents.get(normalizeUri(uri));
}
openDocument(textDocument: TextDocumentItem) {
let document: Document;
if (this.documents.has(textDocument.uri)) {
document = this.get(textDocument.uri) as Document;
document.setText(textDocument.text);
} else {
document = this.createDocument(textDocument);
this.documents.set(normalizeUri(textDocument.uri), document);
this.notify('documentOpen', document);
} }
openDocument(textDocument: TextDocumentItem) { this.notify('documentChange', document);
let document: Document;
if (this.documents.has(textDocument.uri)) {
document = this.get(textDocument.uri) as Document;
document.setText(textDocument.text);
} else {
document = this.createDocument(textDocument);
this.documents.set(normalizeUri(textDocument.uri), document);
this.notify('documentOpen', document);
}
this.notify('documentChange', document); return document;
}
return document; closeDocument(uri: string) {
uri = normalizeUri(uri);
const document = this.documents.get(uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
} }
closeDocument(uri: string) { this.notify('documentClose', document);
uri = normalizeUri(uri);
const document = this.documents.get(uri); // Some plugin may prevent a document from actually being closed.
if (!document) { if (!this.locked.has(uri)) {
throw new Error('Cannot call methods on an unopened document'); this.documents.delete(uri);
} } else {
this.deleteCandidates.add(uri);
this.notify('documentClose', document);
// Some plugin may prevent a document from actually being closed.
if (!this.locked.has(uri)) {
this.documents.delete(uri);
} else {
this.deleteCandidates.add(uri);
}
this.openedInClient.delete(uri);
} }
updateDocument( this.openedInClient.delete(uri);
uri: string, }
changes: TextDocumentContentChangeEvent[]
) {
const document = this.documents.get(normalizeUri(uri));
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
for (const change of changes) { updateDocument(uri: string, changes: TextDocumentContentChangeEvent[]) {
let start = 0; const document = this.documents.get(normalizeUri(uri));
let end = 0; if (!document) {
if ('range' in change) { throw new Error('Cannot call methods on an unopened document');
start = document.offsetAt(change.range.start);
end = document.offsetAt(change.range.end);
} else {
end = document.getTextLength();
}
document.update(change.text, start, end);
}
this.notify('documentChange', document);
} }
markAsOpenedInClient(uri: string) { for (const change of changes) {
this.openedInClient.add(normalizeUri(uri)); let start = 0;
let end = 0;
if ('range' in change) {
start = document.offsetAt(change.range.start);
end = document.offsetAt(change.range.end);
} else {
end = document.getTextLength();
}
document.update(change.text, start, end);
} }
getAllOpenedByClient() { this.notify('documentChange', document);
return Array.from(this.documents.entries()).filter((doc) => }
this.openedInClient.has(doc[0])
);
}
on(name: DocumentEvent, listener: (document: Document) => void) { markAsOpenedInClient(uri: string) {
this.emitter.on(name, listener); this.openedInClient.add(normalizeUri(uri));
} }
private notify(name: DocumentEvent, document: Document) { getAllOpenedByClient() {
this.emitter.emit(name, document); return Array.from(this.documents.entries()).filter((doc) => this.openedInClient.has(doc[0]));
} }
on(name: DocumentEvent, listener: (document: Document) => void) {
this.emitter.on(name, listener);
}
private notify(name: DocumentEvent, document: Document) {
this.emitter.emit(name, document);
}
} }

View file

@ -1,74 +1,77 @@
import { getFirstNonWhitespaceIndex } from './utils'; import { getFirstNonWhitespaceIndex } from './utils';
interface Frontmatter { interface Frontmatter {
state: null | 'open' | 'closed'; state: null | 'open' | 'closed';
startOffset: null | number; startOffset: null | number;
endOffset: null | number; endOffset: null | number;
} }
interface Content { interface Content {
firstNonWhitespaceOffset: null | number; firstNonWhitespaceOffset: null | number;
} }
export interface AstroDocument { export interface AstroDocument {
frontmatter: Frontmatter frontmatter: Frontmatter;
content: Content; content: Content;
} }
/** Parses a document to collect metadata about Astro features */ /** Parses a document to collect metadata about Astro features */
export function parseAstro(content: string): AstroDocument { export function parseAstro(content: string): AstroDocument {
const frontmatter = getFrontmatter(content) const frontmatter = getFrontmatter(content);
return { return {
frontmatter, frontmatter,
content: getContent(content, frontmatter) content: getContent(content, frontmatter),
} };
} }
/** Get frontmatter metadata */ /** Get frontmatter metadata */
function getFrontmatter(content: string): Frontmatter { function getFrontmatter(content: string): Frontmatter {
/** Quickly check how many `---` blocks are in the document */ /** Quickly check how many `---` blocks are in the document */
function getFrontmatterState(): Frontmatter['state'] { function getFrontmatterState(): Frontmatter['state'] {
const parts = content.trim().split('---').length; const parts = content.trim().split('---').length;
switch (parts) { switch (parts) {
case 1: return null; case 1:
case 2: return 'open'; return null;
default: return 'closed'; case 2:
} return 'open';
default:
return 'closed';
} }
const state = getFrontmatterState(); }
const state = getFrontmatterState();
/** Construct a range containing the document's frontmatter */ /** Construct a range containing the document's frontmatter */
function getFrontmatterOffsets(): [number|null, number|null] { function getFrontmatterOffsets(): [number | null, number | null] {
const startOffset = content.indexOf('---'); const startOffset = content.indexOf('---');
if (startOffset === -1) return [null, null]; if (startOffset === -1) return [null, null];
const endOffset = content.slice(startOffset + 3).indexOf('---') + 3; const endOffset = content.slice(startOffset + 3).indexOf('---') + 3;
if (endOffset === -1) return [startOffset, null]; if (endOffset === -1) return [startOffset, null];
return [startOffset, endOffset]; return [startOffset, endOffset];
} }
const [startOffset, endOffset] = getFrontmatterOffsets(); const [startOffset, endOffset] = getFrontmatterOffsets();
return { return {
state, state,
startOffset, startOffset,
endOffset endOffset,
}; };
} }
/** Get content metadata */ /** Get content metadata */
function getContent(content: string, frontmatter: Frontmatter): Content { function getContent(content: string, frontmatter: Frontmatter): Content {
switch (frontmatter.state) { switch (frontmatter.state) {
case null: { case null: {
const offset = getFirstNonWhitespaceIndex(content); const offset = getFirstNonWhitespaceIndex(content);
return { firstNonWhitespaceOffset: offset === -1 ? null : offset } return { firstNonWhitespaceOffset: offset === -1 ? null : offset };
}
case 'open': {
return { firstNonWhitespaceOffset: null }
}
case 'closed': {
const { endOffset } = frontmatter;
const end = (endOffset ?? 0) + 3;
const offset = getFirstNonWhitespaceIndex(content.slice(end))
return { firstNonWhitespaceOffset: end + offset }
}
} }
case 'open': {
return { firstNonWhitespaceOffset: null };
}
case 'closed': {
const { endOffset } = frontmatter;
const end = (endOffset ?? 0) + 3;
const offset = getFirstNonWhitespaceIndex(content.slice(end));
return { firstNonWhitespaceOffset: end + offset };
}
}
} }

View file

@ -1,12 +1,4 @@
import { import { getLanguageService, HTMLDocument, TokenType, ScannerState, Scanner, Node, Position } from 'vscode-html-languageservice';
getLanguageService,
HTMLDocument,
TokenType,
ScannerState,
Scanner,
Node,
Position
} from 'vscode-html-languageservice';
import { Document } from './Document'; import { Document } from './Document';
import { isInsideExpression } from './utils'; import { isInsideExpression } from './utils';
@ -16,154 +8,134 @@ const parser = getLanguageService();
* Parses text as HTML * Parses text as HTML
*/ */
export function parseHtml(text: string): HTMLDocument { export function parseHtml(text: string): HTMLDocument {
const preprocessed = preprocess(text); const preprocessed = preprocess(text);
// We can safely only set getText because only this is used for parsing // We can safely only set getText because only this is used for parsing
const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed }); const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });
return parsedDoc; return parsedDoc;
} }
const createScanner = parser.createScanner as ( const createScanner = parser.createScanner as (input: string, initialOffset?: number, initialState?: ScannerState) => Scanner;
input: string,
initialOffset?: number,
initialState?: ScannerState
) => Scanner;
/** /**
* scan the text and remove any `>` or `<` that cause the tag to end short, * scan the text and remove any `>` or `<` that cause the tag to end short,
*/ */
function preprocess(text: string) { function preprocess(text: string) {
let scanner = createScanner(text); let scanner = createScanner(text);
let token = scanner.scan(); let token = scanner.scan();
let currentStartTagStart: number | null = null; let currentStartTagStart: number | null = null;
while (token !== TokenType.EOS) { while (token !== TokenType.EOS) {
const offset = scanner.getTokenOffset(); const offset = scanner.getTokenOffset();
if (token === TokenType.StartTagOpen) { if (token === TokenType.StartTagOpen) {
currentStartTagStart = offset; currentStartTagStart = offset;
}
if (token === TokenType.StartTagClose) {
if (shouldBlankStartOrEndTagLike(offset)) {
blankStartOrEndTagLike(offset);
} else {
currentStartTagStart = null;
}
}
if (token === TokenType.StartTagSelfClose) {
currentStartTagStart = null;
}
// <Foo checked={a < 1}>
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
if (
token === TokenType.Unknown &&
scanner.getScannerState() === ScannerState.WithinTag &&
scanner.getTokenText() === '<' &&
shouldBlankStartOrEndTagLike(offset)
) {
blankStartOrEndTagLike(offset);
}
token = scanner.scan();
} }
return text; if (token === TokenType.StartTagClose) {
if (shouldBlankStartOrEndTagLike(offset)) {
function shouldBlankStartOrEndTagLike(offset: number) { blankStartOrEndTagLike(offset);
// not null rather than falsy, otherwise it won't work on first tag(0) } else {
return ( currentStartTagStart = null;
currentStartTagStart !== null && }
isInsideExpression(text, currentStartTagStart, offset)
);
} }
function blankStartOrEndTagLike(offset: number) { if (token === TokenType.StartTagSelfClose) {
text = text.substring(0, offset) + ' ' + text.substring(offset + 1); currentStartTagStart = null;
scanner = createScanner(text, offset, ScannerState.WithinTag);
} }
// <Foo checked={a < 1}>
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
if (token === TokenType.Unknown && scanner.getScannerState() === ScannerState.WithinTag && scanner.getTokenText() === '<' && shouldBlankStartOrEndTagLike(offset)) {
blankStartOrEndTagLike(offset);
}
token = scanner.scan();
}
return text;
function shouldBlankStartOrEndTagLike(offset: number) {
// not null rather than falsy, otherwise it won't work on first tag(0)
return currentStartTagStart !== null && isInsideExpression(text, currentStartTagStart, offset);
}
function blankStartOrEndTagLike(offset: number) {
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
scanner = createScanner(text, offset, ScannerState.WithinTag);
}
} }
export interface AttributeContext { export interface AttributeContext {
name: string; name: string;
inValue: boolean; inValue: boolean;
valueRange?: [number, number]; valueRange?: [number, number];
} }
export function getAttributeContextAtPosition( export function getAttributeContextAtPosition(document: Document, position: Position): AttributeContext | null {
document: Document, const offset = document.offsetAt(position);
position: Position const { html } = document;
): AttributeContext | null { const tag = html.findNodeAt(offset);
const offset = document.offsetAt(position);
const { html } = document;
const tag = html.findNodeAt(offset);
if (!inStartTag(offset, tag) || !tag.attributes) {
return null;
}
const text = document.getText();
const beforeStartTagEnd =
text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd));
const scanner = createScanner(beforeStartTagEnd, tag.start);
let token = scanner.scan();
let currentAttributeName: string | undefined;
const inTokenRange = () =>
scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
while (token != TokenType.EOS) {
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
if (token === TokenType.AttributeName) {
currentAttributeName = scanner.getTokenText();
if (inTokenRange()) {
return {
name: currentAttributeName,
inValue: false
};
}
} else if (token === TokenType.DelimiterAssign) {
if (scanner.getTokenEnd() === offset && currentAttributeName) {
const nextToken = scanner.scan();
return {
name: currentAttributeName,
inValue: true,
valueRange: [
offset,
nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset
]
};
}
} else if (token === TokenType.AttributeValue) {
if (inTokenRange() && currentAttributeName) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
const char = text[start];
if (char === '"' || char === "'") {
start++;
end--;
}
return {
name: currentAttributeName,
inValue: true,
valueRange: [start, end]
};
}
currentAttributeName = undefined;
}
token = scanner.scan();
}
if (!inStartTag(offset, tag) || !tag.attributes) {
return null; return null;
}
const text = document.getText();
const beforeStartTagEnd = text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd));
const scanner = createScanner(beforeStartTagEnd, tag.start);
let token = scanner.scan();
let currentAttributeName: string | undefined;
const inTokenRange = () => scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
while (token != TokenType.EOS) {
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
if (token === TokenType.AttributeName) {
currentAttributeName = scanner.getTokenText();
if (inTokenRange()) {
return {
name: currentAttributeName,
inValue: false,
};
}
} else if (token === TokenType.DelimiterAssign) {
if (scanner.getTokenEnd() === offset && currentAttributeName) {
const nextToken = scanner.scan();
return {
name: currentAttributeName,
inValue: true,
valueRange: [offset, nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset],
};
}
} else if (token === TokenType.AttributeValue) {
if (inTokenRange() && currentAttributeName) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
const char = text[start];
if (char === '"' || char === "'") {
start++;
end--;
}
return {
name: currentAttributeName,
inValue: true,
valueRange: [start, end],
};
}
currentAttributeName = undefined;
}
token = scanner.scan();
}
return null;
} }
function inStartTag(offset: number, node: Node) { function inStartTag(offset: number, node: Node) {
return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd; return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
} }

View file

@ -5,63 +5,52 @@ import { clamp } from '../../utils';
* Gets word range at position. * Gets word range at position.
* Delimiter is by default a whitespace, but can be adjusted. * Delimiter is by default a whitespace, but can be adjusted.
*/ */
export function getWordRangeAt( export function getWordRangeAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): { start: number; end: number } {
str: string, let start = str.slice(0, pos).search(delimiterRegex.left);
pos: number, if (start < 0) {
delimiterRegex = { left: /\S+$/, right: /\s/ } start = pos;
): { start: number; end: number } { }
let start = str.slice(0, pos).search(delimiterRegex.left);
if (start < 0) {
start = pos;
}
let end = str.slice(pos).search(delimiterRegex.right); let end = str.slice(pos).search(delimiterRegex.right);
if (end < 0) { if (end < 0) {
end = str.length; end = str.length;
} else { } else {
end = end + pos; end = end + pos;
} }
return { start, end }; return { start, end };
} }
/** /**
* Gets word at position. * Gets word at position.
* Delimiter is by default a whitespace, but can be adjusted. * Delimiter is by default a whitespace, but can be adjusted.
*/ */
export function getWordAt( export function getWordAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): string {
str: string, const { start, end } = getWordRangeAt(str, pos, delimiterRegex);
pos: number, return str.slice(start, end);
delimiterRegex = { left: /\S+$/, right: /\s/ }
): string {
const { start, end } = getWordRangeAt(str, pos, delimiterRegex);
return str.slice(start, end);
} }
/** /**
* Gets index of first-non-whitespace character. * Gets index of first-non-whitespace character.
*/ */
export function getFirstNonWhitespaceIndex(str: string): number { export function getFirstNonWhitespaceIndex(str: string): number {
return str.length - str.trimStart().length; return str.length - str.trimStart().length;
} }
/** checks if a position is currently inside of an expression */ /** checks if a position is currently inside of an expression */
export function isInsideExpression(html: string, tagStart: number, position: number) { export function isInsideExpression(html: string, tagStart: number, position: number) {
const charactersInNode = html.substring(tagStart, position); const charactersInNode = html.substring(tagStart, position);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
} }
/** /**
* Returns if a given offset is inside of the document frontmatter * Returns if a given offset is inside of the document frontmatter
*/ */
export function isInsideFrontmatter( export function isInsideFrontmatter(text: string, offset: number): boolean {
text: string, let start = text.slice(0, offset).trim().split('---').length;
offset: number let end = text.slice(offset).trim().split('---').length;
): boolean {
let start = text.slice(0, offset).trim().split('---').length;
let end = text.slice(offset).trim().split('---').length;
return start > 1 && start < 3 && end >= 1; return start > 1 && start < 3 && end >= 1;
} }
/** /**
@ -70,28 +59,28 @@ export function isInsideFrontmatter(
* @param text The text for which the position should be retrived * @param text The text for which the position should be retrived
*/ */
export function positionAt(offset: number, text: string): Position { export function positionAt(offset: number, text: string): Position {
offset = clamp(offset, 0, text.length); offset = clamp(offset, 0, text.length);
const lineOffsets = getLineOffsets(text); const lineOffsets = getLineOffsets(text);
let low = 0; let low = 0;
let high = lineOffsets.length; let high = lineOffsets.length;
if (high === 0) { if (high === 0) {
return Position.create(0, offset); return Position.create(0, offset);
}
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (lineOffsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
} }
}
while (low < high) { // low is the least x for which the line offset is larger than the current offset
const mid = Math.floor((low + high) / 2); // or array.length if no line offset is larger than the current offset
if (lineOffsets[mid] > offset) { const line = low - 1;
high = mid; return Position.create(line, offset - lineOffsets[line]);
} else {
low = mid + 1;
}
}
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return Position.create(line, offset - lineOffsets[line]);
} }
/** /**
@ -100,40 +89,39 @@ export function positionAt(offset: number, text: string): Position {
* @param text The text for which the offset should be retrived * @param text The text for which the offset should be retrived
*/ */
export function offsetAt(position: Position, text: string): number { export function offsetAt(position: Position, text: string): number {
const lineOffsets = getLineOffsets(text); const lineOffsets = getLineOffsets(text);
if (position.line >= lineOffsets.length) { if (position.line >= lineOffsets.length) {
return text.length; return text.length;
} else if (position.line < 0) { } else if (position.line < 0) {
return 0; return 0;
} }
const lineOffset = lineOffsets[position.line]; const lineOffset = lineOffsets[position.line];
const nextLineOffset = const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
return clamp(nextLineOffset, lineOffset, lineOffset + position.character); return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
} }
function getLineOffsets(text: string) { function getLineOffsets(text: string) {
const lineOffsets = []; const lineOffsets = [];
let isLineStart = true; let isLineStart = true;
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
if (isLineStart) { if (isLineStart) {
lineOffsets.push(i); lineOffsets.push(i);
isLineStart = false; isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
} }
const ch = text.charAt(i);
if (isLineStart && text.length > 0) { isLineStart = ch === '\r' || ch === '\n';
lineOffsets.push(text.length); if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
} }
}
return lineOffsets; if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
} }

View file

@ -71,10 +71,12 @@ export function startServer() {
connection.onDidChangeTextDocument((evt) => docManager.updateDocument(evt.textDocument.uri, evt.contentChanges)); connection.onDidChangeTextDocument((evt) => docManager.updateDocument(evt.textDocument.uri, evt.contentChanges));
connection.onDidChangeWatchedFiles((evt) => { connection.onDidChangeWatchedFiles((evt) => {
const params = evt.changes.map(change => ({ const params = evt.changes
fileName: urlToPath(change.uri), .map((change) => ({
changeType: change.type fileName: urlToPath(change.uri),
})).filter(change => !!change.fileName) changeType: change.type,
}))
.filter((change) => !!change.fileName);
pluginHost.onWatchFileChanges(params); pluginHost.onWatchFileChanges(params);
}); });

View file

@ -1,11 +1,4 @@
import { CompletionContext, CompletionItem, CompletionList, Position, TextDocumentIdentifier } from 'vscode-languageserver';
import {
CompletionContext,
CompletionItem,
CompletionList,
Position,
TextDocumentIdentifier,
} from 'vscode-languageserver';
import type { DocumentManager } from '../core/documents'; import type { DocumentManager } from '../core/documents';
import type * as d from './interfaces'; import type * as d from './interfaces';
import { flatten } from '../utils'; import { flatten } from '../utils';
@ -13,154 +6,107 @@ import { FoldingRange } from 'vscode-languageserver-types';
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
enum ExecuteMode { enum ExecuteMode {
None, None,
FirstNonNull, FirstNonNull,
Collect Collect,
} }
export class PluginHost { export class PluginHost {
private plugins: d.Plugin[] = []; private plugins: d.Plugin[] = [];
constructor(private documentsManager: DocumentManager) {} constructor(private documentsManager: DocumentManager) {}
register(plugin: d.Plugin) { register(plugin: d.Plugin) {
this.plugins.push(plugin); this.plugins.push(plugin);
}
async getCompletions(textDocument: TextDocumentIdentifier, position: Position, completionContext?: CompletionContext): Promise<CompletionList> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
} }
async getCompletions( const completions = (await this.execute<CompletionList>('getCompletions', [document, position, completionContext], ExecuteMode.Collect)).filter(
textDocument: TextDocumentIdentifier, (completion) => completion != null
position: Position, );
completionContext?: CompletionContext
): Promise<CompletionList> { let flattenedCompletions = flatten(completions.map((completion) => completion.items));
const document = this.getDocument(textDocument.uri); const isIncomplete = completions.reduce((incomplete, completion) => incomplete || completion.isIncomplete, false as boolean);
if (!document) {
throw new Error('Cannot call methods on an unopened document'); return CompletionList.create(flattenedCompletions, isIncomplete);
}
async resolveCompletion(textDocument: TextDocumentIdentifier, completionItem: d.AppCompletionItem): Promise<CompletionItem> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
const result = await this.execute<CompletionItem>('resolveCompletion', [document, completionItem], ExecuteMode.FirstNonNull);
return result ?? completionItem;
}
async doTagComplete(textDocument: TextDocumentIdentifier, position: Position): Promise<string | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
return this.execute<string | null>('doTagComplete', [document, position], ExecuteMode.FirstNonNull);
}
async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise<FoldingRange[] | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
const foldingRanges = flatten(await this.execute<FoldingRange[]>('getFoldingRanges', [document], ExecuteMode.Collect)).filter((completion) => completion != null);
return foldingRanges;
}
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
for (const support of this.plugins) {
support.onWatchFileChanges?.(onWatchFileChangesParams);
}
}
private getDocument(uri: string) {
return this.documentsManager.get(uri);
}
private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.FirstNonNull): Promise<T | null>;
private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.Collect): Promise<T[]>;
private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>;
private async execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode): Promise<(T | null) | T[] | void> {
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
switch (mode) {
case ExecuteMode.FirstNonNull:
for (const plugin of plugins) {
const res = await this.tryExecutePlugin(plugin, name, args, null);
if (res != null) {
return res;
}
} }
return null;
const completions = ( case ExecuteMode.Collect:
await this.execute<CompletionList>( return Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])));
'getCompletions', case ExecuteMode.None:
[document, position, completionContext], await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
ExecuteMode.Collect return;
)
).filter((completion) => completion != null);
let flattenedCompletions = flatten(completions.map((completion) => completion.items));
const isIncomplete = completions.reduce(
(incomplete, completion) => incomplete || completion.isIncomplete,
false as boolean
);
return CompletionList.create(flattenedCompletions, isIncomplete);
} }
}
async resolveCompletion( private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) {
textDocument: TextDocumentIdentifier, try {
completionItem: d.AppCompletionItem return await plugin[fnName](...args);
): Promise<CompletionItem> { } catch (e) {
const document = this.getDocument(textDocument.uri); console.error(e);
return failValue;
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
const result = await this.execute<CompletionItem>(
'resolveCompletion',
[document, completionItem],
ExecuteMode.FirstNonNull
);
return result ?? completionItem;
}
async doTagComplete(
textDocument: TextDocumentIdentifier,
position: Position
): Promise<string | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
return this.execute<string | null>(
'doTagComplete',
[document, position],
ExecuteMode.FirstNonNull
);
}
async getFoldingRanges(
textDocument: TextDocumentIdentifier
): Promise<FoldingRange[]|null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
const foldingRanges = flatten(await this.execute<FoldingRange[]>(
'getFoldingRanges',
[document],
ExecuteMode.Collect
)).filter((completion) => completion != null)
return foldingRanges;
}
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
for (const support of this.plugins) {
support.onWatchFileChanges?.(onWatchFileChangesParams);
}
}
private getDocument(uri: string) {
return this.documentsManager.get(uri);
}
private execute<T>(
name: keyof d.LSProvider,
args: any[],
mode: ExecuteMode.FirstNonNull
): Promise<T | null>;
private execute<T>(
name: keyof d.LSProvider,
args: any[],
mode: ExecuteMode.Collect
): Promise<T[]>;
private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>;
private async execute<T>(
name: keyof d.LSProvider,
args: any[],
mode: ExecuteMode
): Promise<(T | null) | T[] | void> {
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
switch (mode) {
case ExecuteMode.FirstNonNull:
for (const plugin of plugins) {
const res = await this.tryExecutePlugin(plugin, name, args, null);
if (res != null) {
return res;
}
}
return null;
case ExecuteMode.Collect:
return Promise.all(
plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, []))
);
case ExecuteMode.None:
await Promise.all(
plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null))
);
return;
}
}
private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) {
try {
return await plugin[fnName](...args);
} catch (e) {
console.error(e);
return failValue;
}
} }
}
} }

View file

@ -49,7 +49,7 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
endLine: end.line, endLine: end.line,
endCharacter: end.character, endCharacter: end.character,
kind: FoldingRangeKind.Imports, kind: FoldingRangeKind.Imports,
} },
]; ];
} }

View file

@ -34,15 +34,7 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
isIncomplete: true, isIncomplete: true,
items: [], items: [],
}; };
this.lang.setCompletionParticipants([ this.lang.setCompletionParticipants([getEmmetCompletionParticipants(document, position, 'html', this.configManager.getEmmetConfig(), emmetResults)]);
getEmmetCompletionParticipants(
document,
position,
'html',
this.configManager.getEmmetConfig(),
emmetResults
)
]);
const results = this.lang.doComplete(document, position, html); const results = this.lang.doComplete(document, position, html);
const items = this.toCompletionItems(results.items); const items = this.toCompletionItems(results.items);
@ -54,14 +46,13 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
); );
} }
getFoldingRanges(document: Document): FoldingRange[]|null { getFoldingRanges(document: Document): FoldingRange[] | null {
const html = this.documents.get(document); const html = this.documents.get(document);
if (!html) { if (!html) {
return null; return null;
} }
return this.lang.getFoldingRanges(document); return this.lang.getFoldingRanges(document);
} }
doTagComplete(document: Document, position: Position): string | null { doTagComplete(document: Document, position: Position): string | null {

View file

@ -1,217 +1,167 @@
import { CompletionContext, FileChangeType, LinkedEditingRanges, SemanticTokens, SignatureHelpContext, TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { import {
CompletionContext, CodeAction,
FileChangeType, CodeActionContext,
LinkedEditingRanges, Color,
SemanticTokens, ColorInformation,
SignatureHelpContext, ColorPresentation,
TextDocumentContentChangeEvent CompletionItem,
} from 'vscode-languageserver'; CompletionList,
import { DefinitionLink,
CodeAction, Diagnostic,
CodeActionContext, FormattingOptions,
Color, Hover,
ColorInformation, Location,
ColorPresentation, Position,
CompletionItem, Range,
CompletionList, ReferenceContext,
DefinitionLink, SymbolInformation,
Diagnostic, TextDocumentIdentifier,
FormattingOptions, TextEdit,
Hover, WorkspaceEdit,
Location, SelectionRange,
Position, SignatureHelp,
Range, FoldingRange,
ReferenceContext,
SymbolInformation,
TextDocumentIdentifier,
TextEdit,
WorkspaceEdit,
SelectionRange,
SignatureHelp,
FoldingRange
} from 'vscode-languageserver-types'; } from 'vscode-languageserver-types';
import { Document } from '../core/documents'; import { Document } from '../core/documents';
export type Resolvable<T> = T | Promise<T>; export type Resolvable<T> = T | Promise<T>;
export interface AppCompletionItem<T extends TextDocumentIdentifier = any> extends CompletionItem { export interface AppCompletionItem<T extends TextDocumentIdentifier = any> extends CompletionItem {
data?: T; data?: T;
} }
export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList { export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList {
items: Array<AppCompletionItem<T>>; items: Array<AppCompletionItem<T>>;
} }
export interface DiagnosticsProvider { export interface DiagnosticsProvider {
getDiagnostics(document: Document): Resolvable<Diagnostic[]>; getDiagnostics(document: Document): Resolvable<Diagnostic[]>;
} }
export interface HoverProvider { export interface HoverProvider {
doHover(document: Document, position: Position): Resolvable<Hover | null>; doHover(document: Document, position: Position): Resolvable<Hover | null>;
} }
export interface FoldingRangeProvider { export interface FoldingRangeProvider {
getFoldingRanges(document: Document): Resolvable<FoldingRange[]|null>; getFoldingRanges(document: Document): Resolvable<FoldingRange[] | null>;
} }
export interface CompletionsProvider<T extends TextDocumentIdentifier = any> { export interface CompletionsProvider<T extends TextDocumentIdentifier = any> {
getCompletions( getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Resolvable<AppCompletionList<T> | null>;
document: Document,
position: Position,
completionContext?: CompletionContext
): Resolvable<AppCompletionList<T> | null>;
resolveCompletion?( resolveCompletion?(document: Document, completionItem: AppCompletionItem<T>): Resolvable<AppCompletionItem<T>>;
document: Document,
completionItem: AppCompletionItem<T>
): Resolvable<AppCompletionItem<T>>;
} }
export interface FormattingProvider { export interface FormattingProvider {
formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>; formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>;
} }
export interface TagCompleteProvider { export interface TagCompleteProvider {
doTagComplete(document: Document, position: Position): Resolvable<string | null>; doTagComplete(document: Document, position: Position): Resolvable<string | null>;
} }
export interface DocumentColorsProvider { export interface DocumentColorsProvider {
getDocumentColors(document: Document): Resolvable<ColorInformation[]>; getDocumentColors(document: Document): Resolvable<ColorInformation[]>;
} }
export interface ColorPresentationsProvider { export interface ColorPresentationsProvider {
getColorPresentations( getColorPresentations(document: Document, range: Range, color: Color): Resolvable<ColorPresentation[]>;
document: Document,
range: Range,
color: Color
): Resolvable<ColorPresentation[]>;
} }
export interface DocumentSymbolsProvider { export interface DocumentSymbolsProvider {
getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>; getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>;
} }
export interface DefinitionsProvider { export interface DefinitionsProvider {
getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>; getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>;
} }
export interface BackwardsCompatibleDefinitionsProvider { export interface BackwardsCompatibleDefinitionsProvider {
getDefinitions( getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[] | Location[]>;
document: Document,
position: Position
): Resolvable<DefinitionLink[] | Location[]>;
} }
export interface CodeActionsProvider { export interface CodeActionsProvider {
getCodeActions( getCodeActions(document: Document, range: Range, context: CodeActionContext): Resolvable<CodeAction[]>;
document: Document, executeCommand?(document: Document, command: string, args?: any[]): Resolvable<WorkspaceEdit | string | null>;
range: Range,
context: CodeActionContext
): Resolvable<CodeAction[]>;
executeCommand?(
document: Document,
command: string,
args?: any[]
): Resolvable<WorkspaceEdit | string | null>;
} }
export interface FileRename { export interface FileRename {
oldUri: string; oldUri: string;
newUri: string; newUri: string;
} }
export interface UpdateImportsProvider { export interface UpdateImportsProvider {
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>; updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
} }
export interface RenameProvider { export interface RenameProvider {
rename( rename(document: Document, position: Position, newName: string): Resolvable<WorkspaceEdit | null>;
document: Document, prepareRename(document: Document, position: Position): Resolvable<Range | null>;
position: Position,
newName: string
): Resolvable<WorkspaceEdit | null>;
prepareRename(document: Document, position: Position): Resolvable<Range | null>;
} }
export interface FindReferencesProvider { export interface FindReferencesProvider {
findReferences( findReferences(document: Document, position: Position, context: ReferenceContext): Promise<Location[] | null>;
document: Document,
position: Position,
context: ReferenceContext
): Promise<Location[] | null>;
} }
export interface SignatureHelpProvider { export interface SignatureHelpProvider {
getSignatureHelp( getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined): Resolvable<SignatureHelp | null>;
document: Document,
position: Position,
context: SignatureHelpContext | undefined
): Resolvable<SignatureHelp | null>;
} }
export interface SelectionRangeProvider { export interface SelectionRangeProvider {
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>; getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
} }
export interface SemanticTokensProvider { export interface SemanticTokensProvider {
getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>; getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>;
} }
export interface LinkedEditingRangesProvider { export interface LinkedEditingRangesProvider {
getLinkedEditingRanges( getLinkedEditingRanges(document: Document, position: Position): Resolvable<LinkedEditingRanges | null>;
document: Document,
position: Position
): Resolvable<LinkedEditingRanges | null>;
} }
export interface OnWatchFileChangesPara { export interface OnWatchFileChangesPara {
fileName: string; fileName: string;
changeType: FileChangeType; changeType: FileChangeType;
} }
export interface OnWatchFileChanges { export interface OnWatchFileChanges {
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void; onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
} }
export interface UpdateTsOrJsFile { export interface UpdateTsOrJsFile {
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void; updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void;
} }
type ProviderBase = DiagnosticsProvider & type ProviderBase = DiagnosticsProvider &
HoverProvider & HoverProvider &
CompletionsProvider & CompletionsProvider &
FormattingProvider & FormattingProvider &
FoldingRangeProvider & FoldingRangeProvider &
TagCompleteProvider & TagCompleteProvider &
DocumentColorsProvider & DocumentColorsProvider &
ColorPresentationsProvider & ColorPresentationsProvider &
DocumentSymbolsProvider & DocumentSymbolsProvider &
UpdateImportsProvider & UpdateImportsProvider &
CodeActionsProvider & CodeActionsProvider &
FindReferencesProvider & FindReferencesProvider &
RenameProvider & RenameProvider &
SignatureHelpProvider & SignatureHelpProvider &
SemanticTokensProvider & SemanticTokensProvider &
LinkedEditingRangesProvider; LinkedEditingRangesProvider;
export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider; export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider;
export interface LSPProviderConfig { export interface LSPProviderConfig {
/** /**
* Whether or not completion lists that are marked as imcomplete * Whether or not completion lists that are marked as imcomplete
* should be filtered server side. * should be filtered server side.
*/ */
filterIncompleteCompletions: boolean; filterIncompleteCompletions: boolean;
/** /**
* Whether or not getDefinitions supports the LocationLink interface. * Whether or not getDefinitions supports the LocationLink interface.
*/ */
definitionLinkSupport: boolean; definitionLinkSupport: boolean;
} }
export type Plugin = Partial< export type Plugin = Partial<ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>;
ProviderBase &
DefinitionsProvider &
OnWatchFileChanges &
SelectionRangeProvider &
UpdateTsOrJsFile
>;

View file

@ -38,7 +38,7 @@ export class LanguageServiceManager {
const url = urlToPath(curr) as string; const url = urlToPath(curr) as string;
if (fileName.startsWith(url) && curr.length < url.length) return url; if (fileName.startsWith(url) && curr.length < url.length) return url;
return found; return found;
}, '') }, '');
} }
private createDocument = (fileName: string, content: string) => { private createDocument = (fileName: string, content: string) => {

View file

@ -6,328 +6,298 @@ import { pathToUrl } from '../../utils';
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils'; import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
export interface TsFilesSpec { export interface TsFilesSpec {
include?: readonly string[]; include?: readonly string[];
exclude?: readonly string[]; exclude?: readonly string[];
} }
export class SnapshotManager { export class SnapshotManager {
private documents: Map<string, DocumentSnapshot> = new Map(); private documents: Map<string, DocumentSnapshot> = new Map();
private lastLogged = new Date(new Date().getTime() - 60_001); private lastLogged = new Date(new Date().getTime() - 60_001);
private readonly watchExtensions = [ private readonly watchExtensions = [ts.Extension.Dts, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Json];
ts.Extension.Dts,
ts.Extension.Js,
ts.Extension.Jsx,
ts.Extension.Ts,
ts.Extension.Tsx,
ts.Extension.Json
];
constructor( constructor(private projectFiles: string[], private fileSpec: TsFilesSpec, private workspaceRoot: string) {}
private projectFiles: string[],
private fileSpec: TsFilesSpec,
private workspaceRoot: string
) {
updateProjectFiles() {
const { include, exclude } = this.fileSpec;
if (include?.length === 0) return;
const projectFiles = ts.sys.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include);
this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
}
updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
const previousSnapshot = this.get(fileName);
if (changes) {
if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) {
return;
}
previousSnapshot.update(changes);
} else {
const newSnapshot = createDocumentSnapshot(fileName);
if (previousSnapshot) {
newSnapshot.version = previousSnapshot.version + 1;
} else {
// ensure it's greater than initial version
// so that ts server picks up the change
newSnapshot.version += 1;
}
this.set(fileName, newSnapshot);
} }
}
updateProjectFiles() { has(fileName: string) {
const { include, exclude } = this.fileSpec; return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
}
if (include?.length === 0) return; get(fileName: string) {
return this.documents.get(fileName);
}
const projectFiles = ts.sys.readDirectory( set(fileName: string, snapshot: DocumentSnapshot) {
this.workspaceRoot, // const prev = this.get(fileName);
this.watchExtensions, this.logStatistics();
exclude, return this.documents.set(fileName, snapshot);
include }
);
this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles])); delete(fileName: string) {
} this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
return this.documents.delete(fileName);
updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void { }
const previousSnapshot = this.get(fileName);
getFileNames() {
if (changes) { return Array.from(this.documents.keys()).map((fileName) => toVirtualAstroFilePath(fileName));
if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) { }
return;
} getProjectFileNames() {
previousSnapshot.update(changes); return [...this.projectFiles];
} else { }
const newSnapshot = createDocumentSnapshot(fileName);
private logStatistics() {
if (previousSnapshot) { const date = new Date();
newSnapshot.version = previousSnapshot.version + 1; // Don't use setInterval because that will keep tests running forever
} else { if (date.getTime() - this.lastLogged.getTime() > 60_000) {
// ensure it's greater than initial version this.lastLogged = date;
// so that ts server picks up the change
newSnapshot.version += 1; const projectFiles = this.getProjectFileNames();
} const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
this.set(fileName, newSnapshot); console.log(
} 'SnapshotManager File Statistics:\n' +
} `Project files: ${projectFiles.length}\n` +
`Astro files: ${allFiles.filter((name) => name.endsWith('.astro')).length}\n` +
has(fileName: string) { `From node_modules: ${allFiles.filter((name) => name.includes('node_modules')).length}\n` +
return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName); `Total: ${allFiles.length}`
} );
get(fileName: string) {
return this.documents.get(fileName);
}
set(fileName: string, snapshot: DocumentSnapshot) {
// const prev = this.get(fileName);
this.logStatistics();
return this.documents.set(fileName, snapshot);
}
delete(fileName: string) {
this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
return this.documents.delete(fileName);
}
getFileNames() {
return Array.from(this.documents.keys()).map(fileName => toVirtualAstroFilePath(fileName));
}
getProjectFileNames() {
return [...this.projectFiles];
}
private logStatistics() {
const date = new Date();
// Don't use setInterval because that will keep tests running forever
if (date.getTime() - this.lastLogged.getTime() > 60_000) {
this.lastLogged = date;
const projectFiles = this.getProjectFileNames();
const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
console.log(
'SnapshotManager File Statistics:\n' +
`Project files: ${projectFiles.length}\n` +
`Astro files: ${
allFiles.filter((name) => name.endsWith('.astro')).length
}\n` +
`From node_modules: ${
allFiles.filter((name) => name.includes('node_modules')).length
}\n` +
`Total: ${allFiles.length}`
);
}
} }
}
} }
export interface DocumentSnapshot extends ts.IScriptSnapshot { export interface DocumentSnapshot extends ts.IScriptSnapshot {
version: number; version: number;
filePath: string; filePath: string;
scriptKind: ts.ScriptKind; scriptKind: ts.ScriptKind;
positionAt(offset: number): Position; positionAt(offset: number): Position;
/** /**
* Instantiates a source mapper. * Instantiates a source mapper.
* `destroyFragment` needs to be called when * `destroyFragment` needs to be called when
* it's no longer needed / the class should be cleaned up * it's no longer needed / the class should be cleaned up
* in order to prevent memory leaks. * in order to prevent memory leaks.
*/ */
getFragment(): Promise<DocumentFragmentSnapshot>; getFragment(): Promise<DocumentFragmentSnapshot>;
/** /**
* Needs to be called when source mapper * Needs to be called when source mapper
* is no longer needed / the class should be cleaned up * is no longer needed / the class should be cleaned up
* in order to prevent memory leaks. * in order to prevent memory leaks.
*/ */
destroyFragment(): void; destroyFragment(): void;
/** /**
* Convenience function for getText(0, getLength()) * Convenience function for getText(0, getLength())
*/ */
getFullText(): string; getFullText(): string;
} }
export const createDocumentSnapshot = (filePath: string, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => { export const createDocumentSnapshot = (filePath: string, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
const text = ts.sys.readFile(filePath) ?? ''; const text = ts.sys.readFile(filePath) ?? '';
if (isAstroFilePath(filePath)) { if (isAstroFilePath(filePath)) {
if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided'); if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided');
const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text)); const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text));
return snapshot; return snapshot;
} }
return new TypeScriptDocumentSnapshot(0, filePath, text); return new TypeScriptDocumentSnapshot(0, filePath, text);
};
}
class AstroDocumentSnapshot implements DocumentSnapshot { class AstroDocumentSnapshot implements DocumentSnapshot {
version = this.doc.version;
scriptKind = ts.ScriptKind.Unknown;
version = this.doc.version; constructor(private doc: Document) {}
scriptKind = ts.ScriptKind.Unknown;
constructor(private doc: Document) {} async getFragment(): Promise<DocumentFragmentSnapshot> {
return new DocumentFragmentSnapshot(this.doc);
}
async getFragment(): Promise<DocumentFragmentSnapshot> { async destroyFragment() {
return new DocumentFragmentSnapshot(this.doc); return;
} }
async destroyFragment() { get text() {
return; return this.doc.getText();
} }
get text() { get filePath() {
return this.doc.getText(); return this.doc.getFilePath() || '';
} }
get filePath() { getText(start: number, end: number) {
return this.doc.getFilePath() || ''; return this.text.substring(start, end);
} }
getText(start: number, end: number) { getLength() {
return this.text.substring(start, end); return this.text.length;
} }
getLength() { getFullText() {
return this.text.length; return this.text;
} }
getFullText() { getChangeRange() {
return this.text; return undefined;
} }
getChangeRange() { positionAt(offset: number) {
return undefined; return positionAt(offset, this.text);
} }
positionAt(offset: number) { getLineContainingOffset(offset: number) {
return positionAt(offset, this.text); const chunks = this.getText(0, offset).split('\n');
} return chunks[chunks.length - 1];
}
getLineContainingOffset(offset: number) {
const chunks = this.getText(0, offset).split('\n');
return chunks[chunks.length - 1];
}
offsetAt(position: Position) {
return offsetAt(position, this.text);
}
offsetAt(position: Position) {
return offsetAt(position, this.text);
}
} }
class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment'|'destroyFragment'> { class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'> {
version: number;
filePath: string;
url: string;
text: string;
version: number; scriptKind = ts.ScriptKind.TSX;
filePath: string; scriptInfo = null;
url: string;
text: string;
scriptKind = ts.ScriptKind.TSX; constructor(private doc: Document) {
scriptInfo = null; const filePath = doc.getFilePath();
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
const text = doc.getText();
this.version = doc.version;
this.filePath = toVirtualAstroFilePath(filePath);
this.url = toVirtualAstroFilePath(filePath);
this.text = this.transformContent(text);
}
constructor( /** @internal */
private doc: Document private transformContent(content: string) {
) { return content.replace(/---/g, '///');
const filePath = doc.getFilePath(); }
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
const text = doc.getText();
this.version = doc.version;
this.filePath = toVirtualAstroFilePath(filePath);
this.url = toVirtualAstroFilePath(filePath);
this.text = this.transformContent(text);
}
/** @internal */ getText(start: number, end: number) {
private transformContent(content: string) { return this.text.substring(start, end);
return content.replace(/---/g, '///'); }
}
getText(start: number, end: number) { getLength() {
return this.text.substring(start, end); return this.text.length;
} }
getLength() { getFullText() {
return this.text.length; return this.text;
} }
getFullText() { getChangeRange() {
return this.text; return undefined;
} }
getChangeRange() { positionAt(offset: number) {
return undefined; return positionAt(offset, this.text);
} }
positionAt(offset: number) { getLineContainingOffset(offset: number) {
return positionAt(offset, this.text); const chunks = this.getText(0, offset).split('\n');
} return chunks[chunks.length - 1];
}
getLineContainingOffset(offset: number) { offsetAt(position: Position): number {
const chunks = this.getText(0, offset).split('\n'); return offsetAt(position, this.text);
return chunks[chunks.length - 1]; }
}
offsetAt(position: Position): number {
return offsetAt(position, this.text);
}
} }
class TypeScriptDocumentSnapshot implements DocumentSnapshot { class TypeScriptDocumentSnapshot implements DocumentSnapshot {
scriptKind = getScriptKindFromFileName(this.filePath);
scriptInfo = null;
url: string;
scriptKind = getScriptKindFromFileName(this.filePath); constructor(public version: number, public readonly filePath: string, private text: string) {
scriptInfo = null; this.url = pathToUrl(filePath);
url: string; }
getText(start: number, end: number) {
return this.text.substring(start, end);
}
constructor(public version: number, public readonly filePath: string, private text: string) { getLength() {
this.url = pathToUrl(filePath) return this.text.length;
}
getFullText() {
return this.text;
}
getChangeRange() {
return undefined;
}
positionAt(offset: number) {
return positionAt(offset, this.text);
}
offsetAt(position: Position): number {
return offsetAt(position, this.text);
}
async getFragment(): Promise<DocumentFragmentSnapshot> {
return (this as unknown) as any;
}
destroyFragment() {
// nothing to clean up
}
getLineContainingOffset(offset: number) {
const chunks = this.getText(0, offset).split('\n');
return chunks[chunks.length - 1];
}
update(changes: TextDocumentContentChangeEvent[]): void {
for (const change of changes) {
let start = 0;
let end = 0;
if ('range' in change) {
start = this.offsetAt(change.range.start);
end = this.offsetAt(change.range.end);
} else {
end = this.getLength();
}
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
} }
getText(start: number, end: number) { this.version++;
return this.text.substring(start, end); }
}
getLength() {
return this.text.length;
}
getFullText() {
return this.text;
}
getChangeRange() {
return undefined;
}
positionAt(offset: number) {
return positionAt(offset, this.text);
}
offsetAt(position: Position): number {
return offsetAt(position, this.text);
}
async getFragment(): Promise<DocumentFragmentSnapshot> {
return this as unknown as any;
}
destroyFragment() {
// nothing to clean up
}
getLineContainingOffset(offset: number) {
const chunks = this.getText(0, offset).split('\n');
return chunks[chunks.length - 1];
}
update(changes: TextDocumentContentChangeEvent[]): void {
for (const change of changes) {
let start = 0;
let end = 0;
if ('range' in change) {
start = this.offsetAt(change.range.start);
end = this.offsetAt(change.range.end);
} else {
end = this.getLength();
}
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
}
this.version++;
}
} }

View file

@ -1,11 +1,7 @@
import type { Document, DocumentManager } from '../../core/documents'; import type { Document, DocumentManager } from '../../core/documents';
import type { ConfigManager } from '../../core/config'; import type { ConfigManager } from '../../core/config';
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces'; import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
import { import { CompletionContext, Position, FileChangeType } from 'vscode-languageserver';
CompletionContext,
Position,
FileChangeType
} from 'vscode-languageserver';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider'; import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
import { LanguageServiceManager } from './LanguageServiceManager'; import { LanguageServiceManager } from './LanguageServiceManager';
@ -13,77 +9,61 @@ import { SnapshotManager } from './SnapshotManager';
import { getScriptKindFromFileName } from './utils'; import { getScriptKindFromFileName } from './utils';
export class TypeScriptPlugin implements CompletionsProvider { export class TypeScriptPlugin implements CompletionsProvider {
private readonly docManager: DocumentManager; private readonly docManager: DocumentManager;
private readonly configManager: ConfigManager; private readonly configManager: ConfigManager;
private readonly languageServiceManager: LanguageServiceManager; private readonly languageServiceManager: LanguageServiceManager;
private readonly completionProvider: CompletionsProviderImpl; private readonly completionProvider: CompletionsProviderImpl;
constructor( constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
docManager: DocumentManager, this.docManager = docManager;
configManager: ConfigManager, this.configManager = configManager;
workspaceUris: string[] this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris);
) {
this.docManager = docManager;
this.configManager = configManager;
this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris);
this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager); this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager);
} }
async getCompletions( async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
document: Document, const completions = await this.completionProvider.getCompletions(document, position, completionContext);
position: Position,
completionContext?: CompletionContext
): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
const completions = await this.completionProvider.getCompletions(
document,
position,
completionContext
);
return completions; return completions;
} }
async resolveCompletion( async resolveCompletion(document: Document, completionItem: AppCompletionItem<CompletionEntryWithIdentifer>): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
document: Document, return this.completionProvider.resolveCompletion(document, completionItem);
completionItem: AppCompletionItem<CompletionEntryWithIdentifer> }
): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
return this.completionProvider.resolveCompletion(document, completionItem);
}
async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> { async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
const doneUpdateProjectFiles = new Set<SnapshotManager>(); const doneUpdateProjectFiles = new Set<SnapshotManager>();
for (const { fileName, changeType } of onWatchFileChangesParams) { for (const { fileName, changeType } of onWatchFileChangesParams) {
const scriptKind = getScriptKindFromFileName(fileName); const scriptKind = getScriptKindFromFileName(fileName);
if (scriptKind === ts.ScriptKind.Unknown) { if (scriptKind === ts.ScriptKind.Unknown) {
// We don't deal with svelte files here // We don't deal with svelte files here
continue; continue;
} }
const snapshotManager = await this.getSnapshotManager(fileName); const snapshotManager = await this.getSnapshotManager(fileName);
if (changeType === FileChangeType.Created) { if (changeType === FileChangeType.Created) {
if (!doneUpdateProjectFiles.has(snapshotManager)) { if (!doneUpdateProjectFiles.has(snapshotManager)) {
snapshotManager.updateProjectFiles(); snapshotManager.updateProjectFiles();
doneUpdateProjectFiles.add(snapshotManager); doneUpdateProjectFiles.add(snapshotManager);
}
} else if (changeType === FileChangeType.Deleted) {
snapshotManager.delete(fileName);
return;
}
snapshotManager.updateProjectFile(fileName);
} }
} } else if (changeType === FileChangeType.Deleted) {
snapshotManager.delete(fileName);
return;
}
/** snapshotManager.updateProjectFile(fileName);
*
* @internal
*/
public async getSnapshotManager(fileName: string) {
return this.languageServiceManager.getSnapshotManager(fileName);
} }
}
/**
*
* @internal
*/
public async getSnapshotManager(fileName: string) {
return this.languageServiceManager.getSnapshotManager(fileName);
}
} }

View file

@ -6,37 +6,37 @@ import { ensureRealAstroFilePath, isAstroFilePath, isVirtualAstroFilePath, toRea
* This should only be accessed by TS Astro module resolution. * This should only be accessed by TS Astro module resolution.
*/ */
export function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapshot) { export function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapshot) {
const AstroSys: ts.System = { const AstroSys: ts.System = {
...ts.sys, ...ts.sys,
fileExists(path: string) { fileExists(path: string) {
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path))); console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path)));
} }
return ts.sys.fileExists(ensureRealAstroFilePath(path)); return ts.sys.fileExists(ensureRealAstroFilePath(path));
}, },
readFile(path: string) { readFile(path: string) {
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
console.log('readFile', path); console.log('readFile', path);
} }
const snapshot = getSnapshot(path); const snapshot = getSnapshot(path);
return snapshot.getFullText(); return snapshot.getFullText();
}, },
readDirectory(path, extensions, exclude, include, depth) { readDirectory(path, extensions, exclude, include, depth) {
const extensionsWithAstro = (extensions ?? []).concat(...['.astro']); const extensionsWithAstro = (extensions ?? []).concat(...['.astro']);
const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);; const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);
return result; return result;
} },
};
if (ts.sys.realpath) {
const realpath = ts.sys.realpath;
AstroSys.realpath = function (path) {
if (isVirtualAstroFilePath(path)) {
return realpath(toRealAstroFilePath(path)) + '.ts';
}
return realpath(path);
}; };
}
if (ts.sys.realpath) { return AstroSys;
const realpath = ts.sys.realpath;
AstroSys.realpath = function (path) {
if (isVirtualAstroFilePath(path)) {
return realpath(toRealAstroFilePath(path)) + '.ts';
}
return realpath(path);
};
}
return AstroSys;
} }

View file

@ -99,7 +99,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
data: { data: {
...comp, ...comp,
uri, uri,
position position,
}, },
}; };
} }

View file

@ -56,27 +56,19 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
let configJson = (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) || getDefaultJsConfig(); let configJson = (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) || getDefaultJsConfig();
if (!configJson.extends) { if (!configJson.extends) {
configJson = Object.assign( configJson = Object.assign(
{ {
exclude: getDefaultExclude() exclude: getDefaultExclude(),
}, },
configJson configJson
); );
} }
const project = ts.parseJsonConfigFileContent( const project = ts.parseJsonConfigFileContent(configJson, parseConfigHost, workspaceRoot, {}, basename(tsconfigPath), undefined, [
configJson, { extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
parseConfigHost, { extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
workspaceRoot, { extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
{}, ]);
basename(tsconfigPath),
undefined,
[
{ extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
{ extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
{ extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }
]
);
let projectVersion = 0; let projectVersion = 0;
const snapshotManager = new SnapshotManager(project.fileNames, { exclude: ['node_modules', 'dist'], include: ['astro'] }, workspaceRoot || process.cwd()); const snapshotManager = new SnapshotManager(project.fileNames, { exclude: ['node_modules', 'dist'], include: ['astro'] }, workspaceRoot || process.cwd());
@ -100,15 +92,15 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
getProjectVersion: () => `${projectVersion}`, getProjectVersion: () => `${projectVersion}`,
getScriptFileNames: () => Array.from(new Set([...snapshotManager.getFileNames(), ...snapshotManager.getProjectFileNames()])), getScriptFileNames: () => Array.from(new Set([...snapshotManager.getFileNames(), ...snapshotManager.getProjectFileNames()])),
getScriptSnapshot, getScriptSnapshot,
getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString() getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString(),
}; };
const languageService = ts.createLanguageService(host); const languageService = ts.createLanguageService(host);
const languageServiceProxy = new Proxy(languageService, { const languageServiceProxy = new Proxy(languageService, {
get(target, prop) { get(target, prop) {
return Reflect.get(target, prop); return Reflect.get(target, prop);
} },
}) });
return { return {
tsconfigPath, tsconfigPath,
@ -141,19 +133,16 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
} }
function getScriptSnapshot(fileName: string): DocumentSnapshot { function getScriptSnapshot(fileName: string): DocumentSnapshot {
fileName = ensureRealAstroFilePath(fileName); fileName = ensureRealAstroFilePath(fileName);
let doc = snapshotManager.get(fileName); let doc = snapshotManager.get(fileName);
if (doc) { if (doc) {
return doc;
}
doc = createDocumentSnapshot(
fileName,
docContext.createDocument,
);
snapshotManager.set(fileName, doc);
return doc; return doc;
}
doc = createDocumentSnapshot(fileName, docContext.createDocument);
snapshotManager.set(fileName, doc);
return doc;
} }
} }
@ -168,7 +157,7 @@ function getDefaultJsConfig(): {
compilerOptions: { compilerOptions: {
maxNodeModuleJsDepth: 2, maxNodeModuleJsDepth: 2,
allowSyntheticDefaultImports: true, allowSyntheticDefaultImports: true,
allowJs: true allowJs: true,
}, },
include: ['astro'], include: ['astro'],
}; };

View file

@ -3,180 +3,172 @@ import { CompletionItemKind, DiagnosticSeverity } from 'vscode-languageserver';
import { dirname } from 'path'; import { dirname } from 'path';
import { pathToUrl } from '../../utils'; import { pathToUrl } from '../../utils';
export function scriptElementKindToCompletionItemKind( export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind {
kind: ts.ScriptElementKind switch (kind) {
): CompletionItemKind { case ts.ScriptElementKind.primitiveType:
switch (kind) { case ts.ScriptElementKind.keyword:
case ts.ScriptElementKind.primitiveType: return CompletionItemKind.Keyword;
case ts.ScriptElementKind.keyword: case ts.ScriptElementKind.constElement:
return CompletionItemKind.Keyword; return CompletionItemKind.Constant;
case ts.ScriptElementKind.constElement: case ts.ScriptElementKind.letElement:
return CompletionItemKind.Constant; case ts.ScriptElementKind.variableElement:
case ts.ScriptElementKind.letElement: case ts.ScriptElementKind.localVariableElement:
case ts.ScriptElementKind.variableElement: case ts.ScriptElementKind.alias:
case ts.ScriptElementKind.localVariableElement: return CompletionItemKind.Variable;
case ts.ScriptElementKind.alias: case ts.ScriptElementKind.memberVariableElement:
return CompletionItemKind.Variable; case ts.ScriptElementKind.memberGetAccessorElement:
case ts.ScriptElementKind.memberVariableElement: case ts.ScriptElementKind.memberSetAccessorElement:
case ts.ScriptElementKind.memberGetAccessorElement: return CompletionItemKind.Field;
case ts.ScriptElementKind.memberSetAccessorElement: case ts.ScriptElementKind.functionElement:
return CompletionItemKind.Field; return CompletionItemKind.Function;
case ts.ScriptElementKind.functionElement: case ts.ScriptElementKind.memberFunctionElement:
return CompletionItemKind.Function; case ts.ScriptElementKind.constructSignatureElement:
case ts.ScriptElementKind.memberFunctionElement: case ts.ScriptElementKind.callSignatureElement:
case ts.ScriptElementKind.constructSignatureElement: case ts.ScriptElementKind.indexSignatureElement:
case ts.ScriptElementKind.callSignatureElement: return CompletionItemKind.Method;
case ts.ScriptElementKind.indexSignatureElement: case ts.ScriptElementKind.enumElement:
return CompletionItemKind.Method; return CompletionItemKind.Enum;
case ts.ScriptElementKind.enumElement: case ts.ScriptElementKind.moduleElement:
return CompletionItemKind.Enum; case ts.ScriptElementKind.externalModuleName:
case ts.ScriptElementKind.moduleElement: return CompletionItemKind.Module;
case ts.ScriptElementKind.externalModuleName: case ts.ScriptElementKind.classElement:
return CompletionItemKind.Module; case ts.ScriptElementKind.typeElement:
case ts.ScriptElementKind.classElement: return CompletionItemKind.Class;
case ts.ScriptElementKind.typeElement: case ts.ScriptElementKind.interfaceElement:
return CompletionItemKind.Class; return CompletionItemKind.Interface;
case ts.ScriptElementKind.interfaceElement: case ts.ScriptElementKind.warning:
return CompletionItemKind.Interface; case ts.ScriptElementKind.scriptElement:
case ts.ScriptElementKind.warning: return CompletionItemKind.File;
case ts.ScriptElementKind.scriptElement: case ts.ScriptElementKind.directory:
return CompletionItemKind.File; return CompletionItemKind.Folder;
case ts.ScriptElementKind.directory: case ts.ScriptElementKind.string:
return CompletionItemKind.Folder; return CompletionItemKind.Constant;
case ts.ScriptElementKind.string: }
return CompletionItemKind.Constant; return CompletionItemKind.Property;
}
return CompletionItemKind.Property;
} }
export function getCommitCharactersForScriptElement( export function getCommitCharactersForScriptElement(kind: ts.ScriptElementKind): string[] | undefined {
kind: ts.ScriptElementKind const commitCharacters: string[] = [];
): string[] | undefined { switch (kind) {
const commitCharacters: string[] = []; case ts.ScriptElementKind.memberGetAccessorElement:
switch (kind) { case ts.ScriptElementKind.memberSetAccessorElement:
case ts.ScriptElementKind.memberGetAccessorElement: case ts.ScriptElementKind.constructSignatureElement:
case ts.ScriptElementKind.memberSetAccessorElement: case ts.ScriptElementKind.callSignatureElement:
case ts.ScriptElementKind.constructSignatureElement: case ts.ScriptElementKind.indexSignatureElement:
case ts.ScriptElementKind.callSignatureElement: case ts.ScriptElementKind.enumElement:
case ts.ScriptElementKind.indexSignatureElement: case ts.ScriptElementKind.interfaceElement:
case ts.ScriptElementKind.enumElement: commitCharacters.push('.');
case ts.ScriptElementKind.interfaceElement: break;
commitCharacters.push('.');
break;
case ts.ScriptElementKind.moduleElement: case ts.ScriptElementKind.moduleElement:
case ts.ScriptElementKind.alias: case ts.ScriptElementKind.alias:
case ts.ScriptElementKind.constElement: case ts.ScriptElementKind.constElement:
case ts.ScriptElementKind.letElement: case ts.ScriptElementKind.letElement:
case ts.ScriptElementKind.variableElement: case ts.ScriptElementKind.variableElement:
case ts.ScriptElementKind.localVariableElement: case ts.ScriptElementKind.localVariableElement:
case ts.ScriptElementKind.memberVariableElement: case ts.ScriptElementKind.memberVariableElement:
case ts.ScriptElementKind.classElement: case ts.ScriptElementKind.classElement:
case ts.ScriptElementKind.functionElement: case ts.ScriptElementKind.functionElement:
case ts.ScriptElementKind.memberFunctionElement: case ts.ScriptElementKind.memberFunctionElement:
commitCharacters.push('.', ','); commitCharacters.push('.', ',');
commitCharacters.push('('); commitCharacters.push('(');
break; break;
} }
return commitCharacters.length === 0 ? undefined : commitCharacters; return commitCharacters.length === 0 ? undefined : commitCharacters;
} }
export function mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity { export function mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity {
switch (category) { switch (category) {
case ts.DiagnosticCategory.Error: case ts.DiagnosticCategory.Error:
return DiagnosticSeverity.Error; return DiagnosticSeverity.Error;
case ts.DiagnosticCategory.Warning: case ts.DiagnosticCategory.Warning:
return DiagnosticSeverity.Warning; return DiagnosticSeverity.Warning;
case ts.DiagnosticCategory.Suggestion: case ts.DiagnosticCategory.Suggestion:
return DiagnosticSeverity.Hint; return DiagnosticSeverity.Hint;
case ts.DiagnosticCategory.Message: case ts.DiagnosticCategory.Message:
return DiagnosticSeverity.Information; return DiagnosticSeverity.Information;
} }
return DiagnosticSeverity.Error; return DiagnosticSeverity.Error;
} }
export function getScriptKindFromFileName(fileName: string): ts.ScriptKind { export function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
const ext = fileName.substr(fileName.lastIndexOf('.')); const ext = fileName.substr(fileName.lastIndexOf('.'));
switch (ext.toLowerCase()) { switch (ext.toLowerCase()) {
case ts.Extension.Js: case ts.Extension.Js:
return ts.ScriptKind.JS; return ts.ScriptKind.JS;
case ts.Extension.Jsx: case ts.Extension.Jsx:
return ts.ScriptKind.JSX; return ts.ScriptKind.JSX;
case ts.Extension.Ts: case ts.Extension.Ts:
return ts.ScriptKind.TS; return ts.ScriptKind.TS;
case ts.Extension.Tsx: case ts.Extension.Tsx:
return ts.ScriptKind.TSX; return ts.ScriptKind.TSX;
case ts.Extension.Json: case ts.Extension.Json:
return ts.ScriptKind.JSON; return ts.ScriptKind.JSON;
default: default:
return ts.ScriptKind.Unknown; return ts.ScriptKind.Unknown;
} }
} }
export function isAstroFilePath(filePath: string) { export function isAstroFilePath(filePath: string) {
return filePath.endsWith('.astro'); return filePath.endsWith('.astro');
} }
export function isVirtualAstroFilePath(filePath: string) { export function isVirtualAstroFilePath(filePath: string) {
return filePath.endsWith('.astro.ts'); return filePath.endsWith('.astro.ts');
} }
export function toVirtualAstroFilePath(filePath: string) { export function toVirtualAstroFilePath(filePath: string) {
return `${filePath}.ts`; return `${filePath}.ts`;
} }
export function toRealAstroFilePath(filePath: string) { export function toRealAstroFilePath(filePath: string) {
return filePath.slice(0, -'.ts'.length); return filePath.slice(0, -'.ts'.length);
} }
export function ensureRealAstroFilePath(filePath: string) { export function ensureRealAstroFilePath(filePath: string) {
return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath; return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath;
} }
export function findTsConfigPath(fileName: string, rootUris: string[]) { export function findTsConfigPath(fileName: string, rootUris: string[]) {
const searchDir = dirname(fileName); const searchDir = dirname(fileName);
const path = const path = ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || '';
ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || // Don't return config files that exceed the current workspace context.
ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
'';
// Don't return config files that exceed the current workspace context.
return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
} }
/** */ /** */
export function isSubPath(uri: string, possibleSubPath: string): boolean { export function isSubPath(uri: string, possibleSubPath: string): boolean {
return pathToUrl(possibleSubPath).startsWith(uri); return pathToUrl(possibleSubPath).startsWith(uri);
} }
/** Substitutes */ /** Substitutes */
export function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { export function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
let accumulatedWS = 0; let accumulatedWS = 0;
result += before; result += before;
for (let i = start + before.length; i < end; i++) { for (let i = start + before.length; i < end; i++) {
let ch = oldContent[i]; let ch = oldContent[i];
if (ch === '\n' || ch === '\r') { if (ch === '\n' || ch === '\r') {
// only write new lines, skip the whitespace // only write new lines, skip the whitespace
accumulatedWS = 0; accumulatedWS = 0;
result += ch; result += ch;
} else { } else {
accumulatedWS++; accumulatedWS++;
} }
} }
result = append(result, ' ', accumulatedWS - after.length); result = append(result, ' ', accumulatedWS - after.length);
result += after; result += after;
return result; return result;
} }
function append(result: string, str: string, n: number): string { function append(result: string, str: string, n: number): string {
while (n > 0) { while (n > 0) {
if (n & 1) { if (n & 1) {
result += str; result += str;
} }
n >>= 1; n >>= 1;
str += str; str += str;
} }
return result; return result;
} }

View file

@ -4,68 +4,61 @@ import { Node } from 'vscode-html-languageservice';
/** Normalizes a document URI */ /** Normalizes a document URI */
export function normalizeUri(uri: string): string { export function normalizeUri(uri: string): string {
return URI.parse(uri).toString(); return URI.parse(uri).toString();
} }
/** Turns a URL into a normalized FS Path */ /** Turns a URL into a normalized FS Path */
export function urlToPath(stringUrl: string): string | null { export function urlToPath(stringUrl: string): string | null {
const url = URI.parse(stringUrl); const url = URI.parse(stringUrl);
if (url.scheme !== 'file') { if (url.scheme !== 'file') {
return null; return null;
} }
return url.fsPath.replace(/\\/g, '/'); return url.fsPath.replace(/\\/g, '/');
} }
/** Converts a path to a URL */ /** Converts a path to a URL */
export function pathToUrl(path: string) { export function pathToUrl(path: string) {
return URI.file(path).toString(); return URI.file(path).toString();
} }
/** /**
* *
* The language service is case insensitive, and would provide * The language service is case insensitive, and would provide
* hover info for Svelte components like `Option` which have * hover info for Svelte components like `Option` which have
* the same name like a html tag. * the same name like a html tag.
*/ */
export function isPossibleComponent(node: Node): boolean { export function isPossibleComponent(node: Node): boolean {
return !!node.tag?.[0].match(/[A-Z]/); return !!node.tag?.[0].match(/[A-Z]/);
} }
/** /**
* *
* The language service is case insensitive, and would provide * The language service is case insensitive, and would provide
* hover info for Svelte components like `Option` which have * hover info for Svelte components like `Option` which have
* the same name like a html tag. * the same name like a html tag.
*/ */
export function isPossibleClientComponent(node: Node): boolean { export function isPossibleClientComponent(node: Node): boolean {
return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1; return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1;
} }
/** Flattens an array */ /** Flattens an array */
export function flatten<T>(arr: T[][]): T[] { export function flatten<T>(arr: T[][]): T[] {
return arr.reduce((all, item) => [...all, ...item], []); return arr.reduce((all, item) => [...all, ...item], []);
} }
/** Clamps a number between min and max */ /** Clamps a number between min and max */
export function clamp(num: number, min: number, max: number): number { export function clamp(num: number, min: number, max: number): number {
return Math.max(min, Math.min(max, num)); return Math.max(min, Math.min(max, num));
} }
/** Checks if a position is inside range */ /** Checks if a position is inside range */
export function isInRange(positionToTest: Position, range: Range): boolean { export function isInRange(positionToTest: Position, range: Range): boolean {
return ( return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start);
isBeforeOrEqualToPosition(range.end, positionToTest) &&
isBeforeOrEqualToPosition(positionToTest, range.start)
);
} }
/** */ /** */
export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean { export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean {
return ( return positionToTest.line < position.line || (positionToTest.line === position.line && positionToTest.character <= position.character);
positionToTest.line < position.line ||
(positionToTest.line === position.line && positionToTest.character <= position.character)
);
} }
/** /**
@ -76,23 +69,19 @@ export function isBeforeOrEqualToPosition(position: Position, positionToTest: Po
* @param determineIfSame The function which determines if the previous invocation should be canceld or not * @param determineIfSame The function which determines if the previous invocation should be canceld or not
* @param miliseconds Number of miliseconds to debounce * @param miliseconds Number of miliseconds to debounce
*/ */
export function debounceSameArg<T>( export function debounceSameArg<T>(fn: (arg: T) => void, shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, miliseconds: number): (arg: T) => void {
fn: (arg: T) => void, let timeout: any;
shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, let prevArg: T | undefined;
miliseconds: number
): (arg: T) => void {
let timeout: any;
let prevArg: T | undefined;
return (arg: T) => { return (arg: T) => {
if (shouldCancelPrevious(arg, prevArg)) { if (shouldCancelPrevious(arg, prevArg)) {
clearTimeout(timeout); clearTimeout(timeout);
} }
prevArg = arg; prevArg = arg;
timeout = setTimeout(() => { timeout = setTimeout(() => {
fn(arg); fn(arg);
prevArg = undefined; prevArg = undefined;
}, miliseconds); }, miliseconds);
}; };
} }

View file

@ -1,9 +1,9 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
} }

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2019", "target": "es2019",
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"sourceMap": true, "sourceMap": true,
"composite": true, "composite": true,
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@astro-vscode/*": ["packages/*/src"] "@astro-vscode/*": ["packages/*/src"]
}, }
}, }
} }

View file

@ -1,14 +1,13 @@
{ {
"extends": "./tsconfig.base.json", "extends": "./tsconfig.base.json",
"files": [], "files": [],
"include": [], "include": [],
"references": [ "references": [
{ {
"path": "./packages/client" "path": "./packages/client"
}, },
{ {
"path": "./packages/server" "path": "./packages/server"
} }
] ]
} }

View file

@ -1,4 +1,3 @@
module.exports = { module.exports = {
workspaceRoot: '../' workspaceRoot: '../',
}; };

View file

@ -1656,7 +1656,7 @@
resolved "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz" resolved "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz"
integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==
"@typescript-eslint/eslint-plugin@^4.18.0": "@typescript-eslint/eslint-plugin@^4.22.0":
version "4.22.0" version "4.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz#3d5f29bb59e61a9dba1513d491b059e536e16dbc" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz#3d5f29bb59e61a9dba1513d491b059e536e16dbc"
integrity sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA== integrity sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==
@ -4278,12 +4278,12 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
eslint-config-prettier@^8.1.0: eslint-config-prettier@^8.3.0:
version "8.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==
eslint-plugin-prettier@^3.3.1: eslint-plugin-prettier@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7"
integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw== integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==
@ -4315,7 +4315,7 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz"
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
eslint@^7.22.0: eslint@^7.25.0:
version "7.25.0" version "7.25.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67"
integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw== integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==