diff --git a/.github/workflows/aleph_in_deno.yml b/.github/workflows/aleph_in_deno.yml index 7dd929056..1b6ebadec 100644 --- a/.github/workflows/aleph_in_deno.yml +++ b/.github/workflows/aleph_in_deno.yml @@ -28,8 +28,8 @@ jobs: with: deno-version: v1.x - - name: Cache std modules - run: deno cache std.ts + - name: Cache deps modules + run: deno cache deps.ts - name: Run tests - run: deno test -A --unstable + run: deno test -A --unstable --location "http://localhost/" diff --git a/.github/workflows/swc.yml b/.github/workflows/swc.yml new file mode 100644 index 000000000..5243f6138 --- /dev/null +++ b/.github/workflows/swc.yml @@ -0,0 +1,37 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: compiler + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ${{ matrix.os }} # runs a test on macOS, Windows and Ubuntu + + strategy: + matrix: + os: [macOS-latest, windows-latest, ubuntu-latest] + + steps: + - name: Setup repo + uses: actions/checkout@v2 + + - name: Setup rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + + - name: Setup wasm-pack + uses: jetli/wasm-pack-action@v0.3.0 + with: + version: latest + + - name: Run tests + run: cd compiler && cargo test --all diff --git a/.gitignore b/.gitignore index 31a283023..0d9789ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store Thumbs.db +compiler/target/ +compiler/pkg/ .aleph/ dist/ -swc/target/ -swc/pkg/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c4eb3fedc..1fb7fba18 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ - "denoland.vscode-deno" + "denoland.vscode-deno", + "rust-lang.rust" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 000dd6b88..e372be344 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "files.eol": "\n", "files.trimTrailingWhitespace": true, + "typescript.format.semicolons": "remove", "[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06abce7d1..09d2252c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,22 +1,27 @@ # Contributing to Aleph.js -Welcome, and thank you for taking time in contributing to Aleph.js! +Welcome, and thank you for taking time in contributing to Aleph.js! You can improve Aleph.js in different ways: -## Code of Conduct - -All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). +- ∆ add new features +- ✘ bugfix +- ✔︎ review code +- ☇ write plugins +- 𝔸 improve our [documentation](https://github.com/alephjs/alephjs.org) ## Development Setup -You will need [Deno](https://deno.land/) 1.5+ and [VS Code](https://code.visualstudio.com/) with [deno extension](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno). +You will need [Deno](https://deno.land/) 1.7+. 1. Fork this repository to your own GitHub account. 2. Clone the repository to your local device. 3. Create a new branch `git checkout -b BRANCH_NAME`. -4. Change code then run examples. +4. Change code then run our examples. ```bash -deno run -A --unstable --import-map=import_map.json cli.ts ./examples/hello-world -L debug +# ssr +deno run -A --unstable --import-map=import_map.json cli.ts dev ./examples/hello-world -L debug +# ssg +deno run -A --unstable --import-map=import_map.json cli.ts build ./examples/hello-world -L debug ``` ## Testing @@ -24,9 +29,22 @@ deno run -A --unstable --import-map=import_map.json cli.ts ./examples/hello-worl Run all tests: ```bash -deno test -A --unstable +deno test -A ``` -## Contributing to Documentation +## Project Structure + +- **/cli** command code +- **/compiler** compiler in rust with swc +- **/framework** framework code +- **/design** design drawings and assets +- **/examples** examples +- **/plugins** official plugins +- **/server** server code +- **/shared** shared code +- **/test** testings +- **/vendor** packages from npm -You are welcome to improve our [documentation](https://alephjs.org/docs). +## Code of Conduct + +All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/LICENSE b/LICENSE index 448e30875..40aa5cc52 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 postUI Lab. +Copyright (c) 2020-2021 postUI Lab. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 34d72139f..df6524fdb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Aleph.js works in **Deno**, a *simple*, *modern* and *secure* runtime for JavaSc import React from "https://esm.sh/react@17.0.1" import Logo from "../components/logo.tsx" -export default function Home() { +export default function App() { return (
@@ -33,6 +33,7 @@ export default function Home() { - Zero Config - Typescript in Deno +- High Performance Comilper - ES Module Ready - Import Maps - HMR with Fast Refresh diff --git a/aleph.ts b/aleph.ts deleted file mode 100644 index 236fccc8e..000000000 --- a/aleph.ts +++ /dev/null @@ -1,233 +0,0 @@ -import React, { ComponentType, useCallback, useEffect, useRef, useState } from 'https://esm.sh/react' -import { RouterContext } from './context.ts' -import { E400MissingDefaultExportAsComponent, E404Page, ErrorBoundary } from './error.ts' -import events from './events.ts' -import { createPageProps, RouteModule, Routing } from './routing.ts' -import type { RouterURL } from './types.ts' -import util, { hashShort, reHttp } from './util.ts' - -export function ALEPH({ initial }: { - initial: { - routing: Routing - url: RouterURL - components: Record> - pageComponentTree: { id: string, Component?: any }[] - } -}) { - const ref = useRef({ routing: initial.routing }) - const [e404, setE404] = useState<{ Component: ComponentType, props?: Record }>(() => { - const { E404 } = initial.components - if (E404) { - if (util.isLikelyReactComponent(E404)) { - return { Component: E404 } - } - return { Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom 404 Page' } } - } - return { Component: E404Page } - }) - const [app, setApp] = useState<{ Component: ComponentType | null, props?: Record }>(() => { - const { App } = initial.components - if (App) { - if (util.isLikelyReactComponent(App)) { - return { Component: App } - } - return { Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom App' } } - } - return { Component: null } - }) - const [route, setRoute] = useState(() => { - const { url, pageComponentTree } = initial - return { ...createPageProps(pageComponentTree), url } - }) - const onpopstate = useCallback(async (e: any) => { - const { routing } = ref.current - const { baseUrl } = routing - const [url, pageModuleTree] = routing.createRouter() - if (url.pagePath !== '') { - const ctree: { id: string, Component?: ComponentType }[] = pageModuleTree.map(({ id }) => ({ id })) - const imports = pageModuleTree.map(async mod => { - const { default: C } = await import(getModuleImportUrl(baseUrl, mod, e.forceRefetch)) - if (mod.deps) { - // import async dependencies - for (const dep of mod.deps.filter(({ isStyle }) => !!isStyle)) { - await import(getModuleImportUrl(baseUrl, { id: util.ensureExt(dep.url.replace(reHttp, '/-/'), '.js'), hash: dep.hash }, e.forceRefetch)) - } - if (mod.deps.filter(({ isData, url }) => !!isData && url.startsWith('#useDeno.')).length > 0) { - const { default: data } = await import(`/_aleph/data${[url.pathname, url.query.toString()].filter(Boolean).join('@')}/data.js` + (e.forceRefetch ? `?t=${Date.now()}` : '')) - if (util.isPlainObject(data)) { - for (const key in data) { - const useDenoUrl = `useDeno://${url.pathname}?${url.query.toString()}#${key}` - Object.assign(window, { [useDenoUrl]: data[key] }) - } - } - } - } - const pc = ctree.find(pc => pc.id === mod.id) - if (pc) { - pc.Component = C - } - }) - await Promise.all(imports) - setRoute({ ...createPageProps(ctree), url }) - if (e.resetScroll) { - (window as any).scrollTo(0, 0) - } - } else { - setRoute({ Page: null, pageProps: {}, url }) - } - }, [ref]) - - useEffect(() => { - window.addEventListener('popstate', onpopstate) - events.on('popstate', onpopstate) - - return () => { - window.removeEventListener('popstate', onpopstate) - events.off('popstate', onpopstate) - } - }, [onpopstate]) - - useEffect(() => { - const { routing } = ref.current - const { baseUrl } = routing - const onAddModule = async (mod: RouteModule) => { - switch (mod.id) { - case '/404.js': { - const { default: Component } = await import(getModuleImportUrl(baseUrl, mod, true)) - if (util.isLikelyReactComponent(Component)) { - setE404({ Component }) - } else { - setE404({ Component: E404Page }) - } - break - } - case '/app.js': { - const { default: Component } = await import(getModuleImportUrl(baseUrl, mod, true)) - if (util.isLikelyReactComponent(Component)) { - setApp({ Component }) - } else { - setApp({ Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom App' } }) - } - break - } - default: { - if (mod.id.startsWith('/pages/')) { - const { routing } = ref.current - routing.update(mod) - events.emit('popstate', { type: 'popstate', forceRefetch: true }) - } - break - } - } - } - const onRemoveModule = (moduleId: string) => { - switch (moduleId) { - case '/404.js': - setE404({ Component: E404Page }) - break - case '/app.js': - setApp({ Component: null }) - break - default: - if (moduleId.startsWith('/pages/')) { - const { routing } = ref.current - routing.removeRoute(moduleId) - events.emit('popstate', { type: 'popstate' }) - } - break - } - } - const onFetchPageModule = async ({ href }: { href: string }) => { - const [pathname, search] = href.split('?') - const [url, pageModuleTree] = routing.createRouter({ pathname, search }) - if (url.pagePath !== '') { - const imports = pageModuleTree.map(async mod => { - await import(getModuleImportUrl(baseUrl, mod)) - if (mod.deps) { - // import async dependencies - for (const dep of mod.deps.filter(({ isStyle }) => !!isStyle)) { - await import(getModuleImportUrl(baseUrl, { id: util.ensureExt(dep.url.replace(reHttp, '/-/'), '.js'), hash: dep.hash })) - } - if (mod.deps.filter(({ isData, url }) => !!isData && url.startsWith('#useDeno.')).length > 0) { - const { default: data } = await import(`/_aleph/data${[url.pathname, url.query.toString()].filter(Boolean).join('@')}/data.js`) - if (util.isPlainObject(data)) { - for (const key in data) { - const useDenoUrl = `useDeno://${url.pathname}?${url.query.toString()}#${key}` - Object.assign(window, { [useDenoUrl]: data[key] }) - } - } - } - } - }) - await Promise.all(imports) - } - } - - events.on('add-module', onAddModule) - events.on('remove-module', onRemoveModule) - events.on('fetch-page-module', onFetchPageModule) - - return () => { - events.off('add-module', onAddModule) - events.off('remove-module', onRemoveModule) - events.off('fetch-page-module', onFetchPageModule) - } - }, [ref]) - - useEffect(() => { - const win = window as any - const { location, document, scrollX, scrollY, hashAnchorScroll } = win - if (location.hash) { - const anchor = document.getElementById(location.hash.slice(1)) - if (anchor) { - const { left, top } = anchor.getBoundingClientRect() - win.scroll({ - top: top + scrollY - (hashAnchorScroll?.offset?.top || 0), - left: left + scrollX - (hashAnchorScroll?.offset?.left || 0), - behavior: hashAnchorScroll?.behavior - }) - } - } - }, [route]) - - return ( - React.createElement( - ErrorBoundary, - null, - React.createElement( - RouterContext.Provider, - { value: route.url }, - ...[ - (route.Page && app.Component) && React.createElement(app.Component, Object.assign({}, app.props, { Page: route.Page, pageProps: route.pageProps })), - (route.Page && !app.Component) && React.createElement(route.Page, route.pageProps), - !route.Page && React.createElement(e404.Component, e404.props) - ].filter(Boolean), - ) - ) - ) -} - -export function getModuleImportUrl(baseUrl: string, mod: RouteModule, forceRefetch = false) { - return util.cleanPath(baseUrl + '/_aleph/' + (mod.id.startsWith('/-/') ? mod.id : util.trimSuffix(mod.id, '.js') + `.${mod.hash.slice(0, hashShort)}.js`) + (forceRefetch ? `?t=${Date.now()}` : '')) -} - -export async function redirect(url: string, replace?: boolean) { - const { location, history } = window as any - - if (!util.isNEString(url)) { - return - } - - if (util.isHttpUrl(url)) { - location.href = url - return - } - - url = util.cleanPath(url) - if (replace) { - history.replaceState(null, '', url) - } else { - history.pushState(null, '', url) - } - events.emit('popstate', { type: 'popstate', resetScroll: true }) -} diff --git a/bootstrap.ts b/bootstrap.ts deleted file mode 100644 index e50cc86db..000000000 --- a/bootstrap.ts +++ /dev/null @@ -1,86 +0,0 @@ -import React, { ComponentType } from 'https://esm.sh/react' -import { hydrate, render } from 'https://esm.sh/react-dom' -import { ALEPH, getModuleImportUrl } from './aleph.ts' -import { Route, RouteModule, Routing } from './routing.ts' -import util, { reHttp } from './util.ts' - -export default async function bootstrap({ - routes, - baseUrl, - defaultLocale, - locales, - preloadModules, - renderMode -}: { - routes: Route[] - baseUrl: string - defaultLocale: string - locales: string[] - preloadModules: RouteModule[], - renderMode: 'ssr' | 'spa' -}) { - const { document } = window as any - const mainEl = document.querySelector('main') - const ssrDataEl = document.querySelector('#ssr-data') - const components: Record = {} - const routing = new Routing(routes, baseUrl, defaultLocale, locales) - const [url, pageModuleTree] = routing.createRouter() - const pageComponentTree: { id: string, Component?: ComponentType }[] = pageModuleTree.map(({ id }) => ({ id })) - const imports = [...preloadModules, ...pageModuleTree].map(async mod => { - const { default: C } = await import(getModuleImportUrl(baseUrl, mod)) - if (mod.deps) { - // import async dependencies - for (const dep of mod.deps.filter(({ isStyle }) => !!isStyle)) { - await import(getModuleImportUrl(baseUrl, { id: util.ensureExt(dep.url.replace(reHttp, '/-/'), '.js'), hash: dep.hash })) - } - } - switch (mod.id) { - case '/app.js': - components['App'] = C - break - case '/404.js': - components['E404'] = C - break - default: - const pc = pageComponentTree.find(pc => pc.id === mod.id) - if (pc) { - pc.Component = C - } - break - } - }) - await Promise.all(imports) - - if (ssrDataEl) { - const ssrData = JSON.parse(ssrDataEl.innerText) - for (const key in ssrData) { - Object.assign(window, { [`useDeno://${url.pathname}?${url.query.toString()}#${key}`]: ssrData[key] }) - } - } - - const el = React.createElement( - ALEPH, - { - initial: { - routing, - url, - components, - pageComponentTree, - } - } - ) - if (renderMode === 'ssr') { - hydrate(el, mainEl) - } else { - render(el, mainEl) - } - - // remove ssr head elements, set a timmer to avoid the tab title flash - setTimeout(() => { - Array.from(document.head.children).forEach((el: any) => { - if (el.hasAttribute('ssr')) { - document.head.removeChild(el) - } - }) - }, 0) -} diff --git a/cli.ts b/cli.ts index 3dbe8f9a6..88b251113 100644 --- a/cli.ts +++ b/cli.ts @@ -1,10 +1,9 @@ -import { Request } from './api.ts' -import { existsDirSync, existsFileSync } from './fs.ts' -import { createHtml } from './html.ts' -import log from './log.ts' -import { getContentType } from './mime.ts' -import { listenAndServe, path, ServerRequest, walk } from './std.ts' -import util from './util.ts' +import { listenAndServe, path, ServerRequest, walk } from './deps.ts' +import { Request } from './server/api.ts' +import log from './server/log.ts' +import { getContentType } from './server/mime.ts' +import { createHtml, existsDirSync, existsFileSync } from './server/util.ts' +import util from './shared/util.ts' import { VERSION } from './version.ts' const commands = { @@ -117,15 +116,14 @@ async function main() { } } - // proxy https://deno.land/x/aleph + // proxy https://deno.land/x/aleph for framework dev if (['dev', 'start', 'build'].includes(command) && existsFileSync('./import_map.json')) { const { imports } = JSON.parse(Deno.readTextFileSync('./import_map.json')) - Object.assign(globalThis, { ALEPH_IMPORT_MAP: { imports } }) if (imports['https://deno.land/x/aleph/']) { - const match = String(imports['https://deno.land/x/aleph/']).match(/^http:\/\/(localhost|127.0.0.1):(\d+)\/$/) + const match = String(imports['https://deno.land/x/aleph/']).match(/^http:\/\/localhost:(\d+)\/$/) if (match) { const cwd = Deno.cwd() - const port = parseInt(match[2]) + const port = parseInt(match[1]) listenAndServe({ port }, async (req: ServerRequest) => { const url = new URL('http://localhost' + req.url) const resp = new Request(req, util.cleanPath(url.pathname), {}, url.searchParams) @@ -155,6 +153,7 @@ async function main() { resp.status(500).send(err.message) } }) + Object.assign(globalThis, { __ALEPH_DEV_PORT: port }) log.info(`Proxy https://deno.land/x/aleph on http://localhost:${port}`) } } diff --git a/cli/build.ts b/cli/build.ts index 0f23f2804..fb32e5e74 100644 --- a/cli/build.ts +++ b/cli/build.ts @@ -1,3 +1,5 @@ +import { Project } from '../server/project.ts' + export const helpMessage = ` Usage: aleph build [...options] @@ -12,7 +14,6 @@ Options: ` export default async function (appDir: string, options: Record) { - const { Project } = await import('../project.ts') const project = new Project(appDir, 'production', Boolean(options.r || options.reload)) await project.build() Deno.exit(0) diff --git a/cli/dev.ts b/cli/dev.ts index 67c3ce72b..6bef09523 100644 --- a/cli/dev.ts +++ b/cli/dev.ts @@ -1,4 +1,5 @@ -import log from '../log.ts' +import log from '../server/log.ts' +import { start } from '../server/server.ts' export const helpMessage = ` Usage: @@ -15,7 +16,6 @@ Options: ` export default async function (appDir: string, options: Record) { - const { start } = await import('../server.ts') const port = parseInt(String(options.p || options.port || '8080')) if (isNaN(port) || port <= 0 || !Number.isInteger(port)) { log.error(`invalid port '${options.port || options.p}'`) diff --git a/cli/init.ts b/cli/init.ts index 018dd4a65..a835a2c76 100644 --- a/cli/init.ts +++ b/cli/init.ts @@ -1,8 +1,8 @@ -import { gzipDecode } from 'https://deno.land/x/wasm_gzip@v1.0.0/mod.ts' -import { ensureTextFile } from '../fs.ts' -import log from '../log.ts' -import { colors, ensureDir, fromStreamReader, path, Untar } from '../std.ts' -import util from '../util.ts' +import { colors, ensureDir, gzipDecode, path, Untar } from '../deps.ts' +import log from '../server/log.ts' +import { ensureTextFile } from '../server/util.ts' +import util from '../shared/util.ts' +import { VERSION } from '../version.ts' const gitignore = [ '.DS_Store', @@ -46,7 +46,7 @@ export default async function (appDir: string, options: Record) { - const { start } = await import('../server.ts') + const { start } = await import('../server/server.ts') const host = String(options.hn || options.hostname || 'localhost') const port = parseInt(String(options.p || options.port || '8080')) if (isNaN(port) || port <= 0 || !Number.isInteger(port)) { diff --git a/cli/upgrade.ts b/cli/upgrade.ts index 2f93254f1..5ef3c9e23 100644 --- a/cli/upgrade.ts +++ b/cli/upgrade.ts @@ -1,4 +1,5 @@ -import { colors } from '../std.ts' +import { colors } from '../deps.ts' +import { VERSION as currentVersion } from '../version.ts' export const helpMessage = ` Usage: @@ -12,25 +13,30 @@ Options: async function run(...cmd: string[]) { const p = Deno.run({ cmd, - stdout: 'piped', + stdout: 'null', stderr: 'piped' }) - Deno.stdout.write(await p.output()) Deno.stderr.write(await p.stderrOutput()) p.close() } export default async function (version: string) { + console.log('Looking up latest version...') const { latest, versions } = await (await fetch('https://cdn.deno.land/aleph/meta/versions.json')).json() if (version === 'latest') { version = latest } else if (!versions.includes(version)) { version = 'v' + version if (!versions.includes(version)) { - console.log(`${colors.red('error')}: version(${version}) not found`) + console.log(`${colors.red('error')}: version(${version}) not found!`) Deno.exit(1) } } + if (version === 'v' + currentVersion) { + console.log('Already up-to-date!') + Deno.exit(0) + } await run('deno', 'install', '-A', '-f', '-n', 'aleph', `https://deno.land/x/aleph@${version}/cli.ts`) + console.log(`Aleph.js is up to ${version}!`) Deno.exit(0) } diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock new file mode 100644 index 000000000..eca71b392 --- /dev/null +++ b/compiler/Cargo.lock @@ -0,0 +1,1260 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aleph-compiler" +version = "0.0.0" +dependencies = [ + "anyhow", + "base64 0.13.0", + "console_error_panic_hook", + "indexmap", + "lazy_static", + "once_cell", + "path-slash", + "pathdiff", + "regex", + "relative-path", + "serde", + "sha-1", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_compat", + "swc_ecma_transforms_proposal", + "swc_ecma_transforms_typescript", + "swc_ecma_utils", + "swc_ecma_visit", + "swc_ecmascript", + "url", + "wasm-bindgen", +] + +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ast_node" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c701db7f0f212e2e3024a1929cdaf9a21815f329c5ef43be951fc163b3cdc567" +dependencies = [ + "darling", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +dependencies = [ + "cfg-if 0.1.10", + "wasm-bindgen", +] + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "enum_kind" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e57153e35187d51f08471d5840459ff29093473e7bedd004a1414985aab92f3" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "from_variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039885ad6579a86b94ad8df696cce8c530da496bf7b07b12fec8d6c4cd654bb9" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7280c75fb2e2fc47080ec80ccc481376923acb04501957fc38f935c3de5088" + +[[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "is-macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04807f3dc9e3ea39af3f8469a5297267faf94859637afb836b33f47d9b2650ee" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "ordered-float" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dacdec97876ef3ede8c50efc429220641a0b11ba0048b4b0c357bccbc47c5204" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "path-slash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619" + +[[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pmutil" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "relative-path" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65aff7c83039e88c1c0b4bedf8dfa93d6ec84d5fc2945b37c1fa4186f46c5f94" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca2a8cb5805ce9e3b95435e3765b7b553cecc762d938d409434338386cb5775" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "siphasher" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "sourcemap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e031f2463ecbdd5f34c950f89f5c1e1032f22c0f8e3dc4bdb2e8b6658cf61eb" +dependencies = [ + "base64 0.11.0", + "if_chain", + "lazy_static", + "regex", + "rustc_version", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_enum" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fdb6536756cfd35ee18b9a9972ab2a699d405cc57e0ad0532022960f30d581" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "swc_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "762f5c66bf70e6f96db67808b5ad783c33a72cc3e0022cd04b41349231cdbe6c" +dependencies = [ + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "swc_common" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8681271be490a61fc0efb2de6897f7a886801b1bd0be021c7d862508bf5147" +dependencies = [ + "ast_node", + "cfg-if 0.1.10", + "either", + "from_variant", + "fxhash", + "log", + "num-bigint", + "once_cell", + "owning_ref", + "scoped-tls", + "serde", + "sourcemap", + "string_cache", + "swc_eq_ignore_macros", + "swc_visit", + "unicode-width", +] + +[[package]] +name = "swc_ecma_ast" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574ea8ff4430584d09c0d93b4a1e41aa5fbb27b7f9ae8f86b069ebd23f064d9c" +dependencies = [ + "is-macro", + "num-bigint", + "serde", + "string_enum", + "swc_atoms", + "swc_common", +] + +[[package]] +name = "swc_ecma_codegen" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15136c31aae5b4cc2c923bbd1275408b83f43ed965fb0a63103e3dd5d2dfbb9" +dependencies = [ + "bitflags", + "num-bigint", + "sourcemap", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen_macros", + "swc_ecma_parser", +] + +[[package]] +name = "swc_ecma_codegen_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6505258e3ef526ded22eadaf3e342e2915ff3b3d9e5273e256c62bb8f62b5686" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "swc_ecma_parser" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6ed277c7f50fa870059326516a34281599517f1d4d7aea2316c8e2cbffc63f" +dependencies = [ + "either", + "enum_kind", + "fxhash", + "log", + "num-bigint", + "serde", + "smallvec", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_transforms" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b54bcb14303bc518c56dbecb80f40e58a7eec6c8b6f2459f8816945fc78da16" +dependencies = [ + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_react", + "swc_ecma_utils", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_transforms_base" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "766e24dda7c7471e46b009b1a3d291e6c2d57d584121206b36ae08fed84f1772" +dependencies = [ + "fxhash", + "once_cell", + "phf", + "scoped-tls", + "smallvec", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_compat" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb8a8aaafc33563922fef77eb725a61edbf7b61d866e97c6feeb4c6078c8bcc" +dependencies = [ + "arrayvec", + "fxhash", + "indexmap", + "is-macro", + "num-bigint", + "ordered-float", + "serde", + "smallvec", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_base", + "swc_ecma_transforms_macros", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516ebfe73851f2bb27666ff1156ca6f7a70ee779e1ad4b792e499acd24a8fedc" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "swc_ecma_transforms_proposal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bd4512ee5c841e1736ec9df6bee07c7a4ce3c58e11b01f6eff878a8b2d9a1c" +dependencies = [ + "either", + "fxhash", + "serde", + "smallvec", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_react" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c13dca07c97af0b6b9ce0605a056f96175a57fc85fe81482598ef06d95515" +dependencies = [ + "dashmap", + "once_cell", + "regex", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_typescript" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce407887d735fed348cb1dc2c4fffe0679bba5b39d4995f93ec2aceb7d48f33d" +dependencies = [ + "fxhash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_utils" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a57895a5d9b0386fbfc69eb2371a4ec46b10ff32a0d01d2c38532310081fa8" +dependencies = [ + "once_cell", + "scoped-tls", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_visit" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256eb6b492f888d8c2a05b1da90e0389861a63f9a9bbea0c8eab6134817e4f66" +dependencies = [ + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", +] + +[[package]] +name = "swc_ecmascript" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ea117dcfb936b96d7795742fd8e6cfcabbed7d0ecb5725f3a4bafcfb6c51b9" +dependencies = [ + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms", + "swc_ecma_visit", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_macros_common" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a9f27d290938370597d363df9a77ba4be8e2bc99f32f69eb5245cdeed3c512" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_visit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca9ac0c9177cbc3ae943f7fa1126831b00b68c49c24a0c07f45647e120871d8" +dependencies = [ + "either", + "swc_visit_macros", +] + +[[package]] +name = "swc_visit_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a544fa1da1a6436b219cb3b47ff3cf140e8eea5b5134d3e21f1c481ca1482186" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml new file mode 100644 index 000000000..fda651b9e --- /dev/null +++ b/compiler/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "aleph-compiler" +version = "0.0.0" +authors = ["The Aleph.js authors"] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +anyhow = "1.0.38" +base64 = "0.13.0" +indexmap = "1.6.1" +lazy_static = "1.4.0" +once_cell = "1.5.2" +path-slash = "0.1.4" +pathdiff = "0.2.0" +regex = "1.4.3" +relative-path = "1.3.2" +sha-1 = "0.9.2" +serde = { version = "1.0.120", features = ["derive"] } +url = "2.2.0" + +# swc +# docs: https://swc.rs +swc_common = { version = "0.10.9", features = ["sourcemap"] } +swc_ecmascript = { version = "0.18.1", features = ["codegen", "parser", "react", "transforms", "visit"] } +swc_ecma_transforms_typescript = "0.3.1" +swc_ecma_transforms_compat = "0.3.0" +swc_ecma_transforms_proposal = "0.3.0" +swc_ecma_ast = "0.37.0" +swc_ecma_visit = "0.23.0" +swc_ecma_utils = "0.27.0" + +# wasm-bindgen +# docs: https://rustwasm.github.io/docs/wasm-bindgen +wasm-bindgen = {version = "0.2.69", features = ["serde-serialize"]} +console_error_panic_hook = { version = "0.1.6", optional = true } + +[profile.release] +# Tell `rustc` to optimize for speed +lto = true diff --git a/compiler/README.md b/compiler/README.md new file mode 100644 index 000000000..eda52f973 --- /dev/null +++ b/compiler/README.md @@ -0,0 +1,25 @@ +# Aleph.js Compiler + +The compiler of Aleph.js written in Rust, powered by [swc](https://github.com/swc-project/swc). + +## Development Setup + +You will need [rust](https://www.rust-lang.org/tools/install) 1.45+ and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). + +## Build + +```bash +deno run -A build.ts +``` + +## Run tests + +```bash +cargo test --all +``` + +## Benchmark + +```bash +deno run -A benchmark.ts +``` diff --git a/compiler/benchmark.ts b/compiler/benchmark.ts new file mode 100644 index 000000000..7d0cdca56 --- /dev/null +++ b/compiler/benchmark.ts @@ -0,0 +1,96 @@ +import { compile, CompileOptions } from 'https://deno.land/x/aleph@v0.2.28/tsc/compile.ts'; +import { colors, path, Sha1, walk } from '../deps.ts'; +import { initWasm, transpileSync } from './mod.ts'; + +const hashShort = 9 +const reHttp = /^https?:\/\//i + +function tsc(source: string, opts: any) { + const compileOptions: CompileOptions = { + mode: opts.isDev ? 'development' : 'production', + target: 'es2020', + reactRefresh: opts.isDev, + rewriteImportPath: (path: string) => path.replace(reHttp, '/-/'), + signUseDeno: (id: string) => { + const sig = 'useDeno.' + (new Sha1()).update(id).update("0.2.25").update(Date.now().toString()).hex().slice(0, hashShort) + return sig + } + } + compile(opts.filename, source, compileOptions) +} + +/** + * colored diff + * - red: 0.0 - 1.0 slower + * - yellow: 1.0 - 10.0 faster + * - green: >= 10.0 faster as expected + */ +function coloredDiff(d: number) { + let cf = colors.green + if (d < 1) { + cf = colors.red + } else if (d < 10) { + cf = colors.yellow + } + return cf(d.toFixed(2) + 'x') +} + +async function benchmark(sourceFiles: Array<{ code: string, filename: string }>, isDev: boolean) { + console.log(`[benchmark] ${sourceFiles.length} files ${isDev ? '(development mode)' : ''}`) + + const d1 = { d: 0, min: 0, max: 0, } + for (const { code, filename } of sourceFiles) { + const t = performance.now() + for (let i = 0; i < 5; i++) { + tsc(code, { filename, isDev }) + } + const d = (performance.now() - t) / 5 + if (d1.min === 0 || d < d1.min) { + d1.min = d + } + if (d > d1.max) { + d1.max = d + } + d1.d += d + } + + const d2 = { d: 0, min: 0, max: 0, } + for (const { code, filename } of sourceFiles) { + const t = performance.now() + for (let i = 0; i < 5; i++) { + transpileSync(code, { url: filename, swcOptions: {}, isDev }) + } + const d = (performance.now() - t) / 5 + if (d2.min === 0 || d < d2.min) { + d2.min = d + } + if (d > d2.max) { + d2.max = d + } + d2.d += d + } + + console.log(`tsc done in ${(d1.d / 1000).toFixed(2)}s, min in ${d1.min.toFixed(2)}ms, max in ${d1.max.toFixed(2)}ms`) + console.log(`swc done in ${(d2.d / 1000).toFixed(2)}s, min in ${d2.min.toFixed(2)}ms, max in ${d2.max.toFixed(2)}ms`) + console.log(`swc is ${coloredDiff(d1.d / d2.d)} ${d1.d > d2.d ? 'faster' : 'slower'} than tsc`) +} + +if (import.meta.main) { + (async () => { + const p = Deno.run({ + cmd: ['deno', 'info'], + stdout: 'piped', + stderr: 'null' + }) + await initWasm((new TextDecoder).decode(await p.output()).split('"')[1]) + + const sourceFiles: Array<{ code: string, filename: string }> = [] + const walkOptions = { includeDirs: false, exts: ['.tsx'], skip: [/[\._]test\.tsx?$/i] } + for await (const { path: filename } of walk(path.resolve('..'), walkOptions)) { + sourceFiles.push({ code: await Deno.readTextFile(filename), filename }) + } + + await benchmark(sourceFiles, true) + await benchmark(sourceFiles, false) + })() +} diff --git a/compiler/build.ts b/compiler/build.ts new file mode 100644 index 000000000..f8d4109a9 --- /dev/null +++ b/compiler/build.ts @@ -0,0 +1,28 @@ +import { base64, brotli, Sha1 } from '../deps.ts' + +if (import.meta.main) { + const p = Deno.run({ + cmd: ['wasm-pack', 'build', '--target', 'web'], + stdout: 'inherit', + stderr: 'inherit' + }) + await p.status() + p.close() + const wasmData = await Deno.readFile('./pkg/aleph_compiler_bg.wasm') + const data = brotli.compress(wasmData) + const data64 = base64.encode(data) + const hash = (new Sha1).update(data).hex() + await Deno.writeTextFile( + './wasm.js', + [ + `import { base64, brotli } from "../deps.ts";`, + `const dataRaw = "${data64}";`, + `export default () => brotli.decompress(base64.decode(dataRaw))` + ].join('\n') + ) + await Deno.writeTextFile( + './wasm-checksum.js', + `export const checksum = ${JSON.stringify(hash)}` + ) + await Deno.copyFile('./pkg/aleph_compiler.js', './wasm-pack.js') +} diff --git a/compiler/mod.ts b/compiler/mod.ts new file mode 100644 index 000000000..d5d9203db --- /dev/null +++ b/compiler/mod.ts @@ -0,0 +1,77 @@ +import { ensureDir } from "https://deno.land/std@0.83.0/fs/ensure_dir.ts"; +import { existsSync, path } from '../deps.ts'; +import { VERSION } from "../version.ts"; +import { checksum } from './wasm-checksum.js'; +import { default as init_wasm, transformSync } from './wasm-pack.js'; + +type ImportMap = Record> + +export interface SWCOptions { + target?: 'es5' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' + jsxFactory?: string + jsxFragmentFactory?: string + sourceType?: 'js' | 'jsx' | 'ts' | 'tsx' + sourceMap?: boolean +} + +export interface TransformOptions { + url: string + importMap?: { imports: ImportMap, scopes: Record } + reactVersion?: string, + swcOptions?: SWCOptions + isDev?: boolean, + bundleMode?: boolean, + bundledPaths?: string[], +} + +interface DependencyDescriptor { + specifier: string, + isDynamic: boolean, +} + +export interface TransformRet { + code: string + map?: string + deps: DependencyDescriptor[] + inlineStyles: Record +} + +/** + * transpile code synchronously by swc. + * + * ```javascript + * transpileSync( + * ` + * export default App() { + * return

Hello World

+ * } + * `, + * { + * url: '/app.tsx' + * swcOptions: { + * target: 'es2020' + * } + * }) + * ``` + */ +export function transpileSync(code: string, opts?: TransformOptions): TransformRet { + return transformSync(code, opts) +} + +/** + * load and initiate compiler wasm. + */ +export const initWasm = async (denoCacheDir: string) => { + const cacheDir = path.join(denoCacheDir, `deps/https/deno.land/aleph@v${VERSION}`) + const cachePath = `${cacheDir}/compiler.${checksum}.wasm` + if (existsSync(cachePath)) { + const wasmData = await Deno.readFile(cachePath) + await init_wasm(wasmData) + } else { + const { default: getWasmData } = await import('./wasm.js') + const wasmData = getWasmData() + await init_wasm(wasmData) + await ensureDir(cacheDir) + await Deno.writeFile(cachePath, wasmData) + } +} diff --git a/compiler/polyfills/es5/polyfill.js b/compiler/polyfills/es5/polyfill.js new file mode 100644 index 000000000..6d02bc33c --- /dev/null +++ b/compiler/polyfills/es5/polyfill.js @@ -0,0 +1,5 @@ +import { mark, wrap } from 'https://esm.sh/regenerator-runtime@0.13.7' + +if (!('regeneratorRuntime' in window)) { + window.regeneratorRuntime = { mark, wrap } +} diff --git a/compiler/src/aleph.rs b/compiler/src/aleph.rs new file mode 100644 index 000000000..78431e2a5 --- /dev/null +++ b/compiler/src/aleph.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +lazy_static! { + pub static ref VERSION: String = { + let ts = include_str!("../../version.ts"); + ts.split("'") + .collect::>() + .get(1) + .unwrap() + .to_string() + }; +} diff --git a/compiler/src/compat_fixer.rs b/compiler/src/compat_fixer.rs new file mode 100644 index 000000000..72cf46136 --- /dev/null +++ b/compiler/src/compat_fixer.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use crate::resolve::is_call_expr_by_name; + +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_utils::quote_ident; +use swc_ecma_visit::{noop_fold_type, Fold}; + +pub fn compat_fixer_fold() -> impl Fold { + CompatFixer {} +} + +struct CompatFixer {} + +impl Fold for CompatFixer { + noop_fold_type!(); + + // - `require("regenerator-runtime")` -> `__ALEPH.require("regenerator-runtime")` + fn fold_call_expr(&mut self, call: CallExpr) -> CallExpr { + if is_call_expr_by_name(&call, "require") { + let ok = match call.args.first() { + Some(ExprOrSpread { expr, .. }) => match expr.as_ref() { + Expr::Lit(lit) => match lit { + Lit::Str(_) => true, + _ => false, + }, + _ => false, + }, + _ => false, + }; + if ok { + return CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("__ALEPH")))), + prop: Box::new(Expr::Ident(quote_ident!("require"))), + computed: false, + }))), + args: call.args, + type_args: None, + }; + } + } + call + } +} diff --git a/compiler/src/error.rs b/compiler/src/error.rs new file mode 100644 index 000000000..adc8d835a --- /dev/null +++ b/compiler/src/error.rs @@ -0,0 +1,64 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use std::{fmt, sync::Arc, sync::RwLock}; +use swc_common::{ + errors::{Diagnostic, DiagnosticBuilder, Emitter}, + FileName, Loc, Span, +}; + +/// A buffer for collecting diagnostic messages from the AST parser. +#[derive(Debug)] +pub struct DiagnosticBuffer(Vec); + +impl fmt::Display for DiagnosticBuffer { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.pad(&self.0.join(",")) + } +} + +impl DiagnosticBuffer { + pub fn from_error_buffer(error_buffer: ErrorBuffer, get_loc: F) -> Self + where + F: Fn(Span) -> Loc, + { + let diagnostics = error_buffer.0.read().unwrap().clone(); + let diagnostics = diagnostics + .iter() + .map(|d| { + let mut message = d.message(); + + if let Some(span) = d.span.primary_span() { + let loc = get_loc(span); + let file_name = match &loc.file.name { + FileName::Real(p) => p.display(), + _ => unreachable!(), + }; + message = format!( + "{} at {}:{}:{}", + message, file_name, loc.line, loc.col_display + ); + } + + message + }) + .collect(); + + Self(diagnostics) + } +} + +/// A buffer for collecting errors from the AST parser. +#[derive(Debug, Clone)] +pub struct ErrorBuffer(Arc>>); + +impl ErrorBuffer { + pub fn new() -> Self { + Self(Arc::new(RwLock::new(Vec::new()))) + } +} + +impl Emitter for ErrorBuffer { + fn emit(&mut self, diagnostic_builder: &DiagnosticBuilder) { + self.0.write().unwrap().push((**diagnostic_builder).clone()); + } +} diff --git a/compiler/src/fast_refresh.rs b/compiler/src/fast_refresh.rs new file mode 100644 index 000000000..af5817626 --- /dev/null +++ b/compiler/src/fast_refresh.rs @@ -0,0 +1,1442 @@ +// Copyright 2020 the Aleph.js authors. All rights reserved. MIT license. + +use indexmap::IndexSet; +use sha1::{Digest, Sha1}; +use std::rc::Rc; +use swc_common::{SourceMap, Spanned, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_utils::{private_ident, quote_ident}; +use swc_ecma_visit::{noop_fold_type, Fold}; + +pub fn fast_refresh_fold( + refresh_reg: &str, + refresh_sig: &str, + emit_full_signatures: bool, + source: Rc, +) -> impl Fold { + FastRefreshFold { + source, + signature_index: 0, + registration_index: 0, + registrations: vec![], + signatures: vec![], + refresh_reg: refresh_reg.into(), + refresh_sig: refresh_sig.into(), + emit_full_signatures, + } +} + +/// aleph.js fast-refresh fold. +/// +/// @ref https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js +pub struct FastRefreshFold { + source: Rc, + signature_index: u32, + registration_index: u32, + registrations: Vec<(Ident, String)>, + signatures: Vec, + refresh_reg: String, + refresh_sig: String, + emit_full_signatures: bool, +} + +#[derive(Clone, Debug)] +struct Signature { + parent_ident: Option, + handle_ident: Ident, + hook_calls: Vec, +} + +#[derive(Clone, Debug)] +struct HookCall { + obj: Option, + ident: Ident, + key: String, + is_builtin: bool, +} + +impl FastRefreshFold { + fn create_registration_handle_ident(&mut self) -> Ident { + let mut registration_handle_name = String::from("_c"); + self.registration_index += 1; + if self.registration_index > 1 { + registration_handle_name.push_str(&self.registration_index.to_string()); + }; + private_ident!(registration_handle_name.as_str()) + } + + fn get_persistent_fn( + &mut self, + bindings: &IndexSet, + ident: Option<&Ident>, + block_stmt: &mut BlockStmt, + ) -> (Option, Option) { + let fc_id = match ident { + Some(ident) => { + if is_componentish_name(ident.as_ref()) { + Some(ident.clone()) + } else { + None + } + } + None => None, + }; + let mut bindings_scope = IndexSet::::new(); + let mut hook_calls = Vec::::new(); + let mut exotic_signatures = Vec::<(usize, Signature, Option)>::new(); + let mut index: usize = 0; + let stmts = &mut block_stmt.stmts; + + // marge top bindings + for id in bindings.iter() { + bindings_scope.insert(id.to_string()); + } + + // collect scope bindings + stmts.into_iter().for_each(|stmt| { + match stmt { + // function useFancyState() {} + Stmt::Decl(Decl::Fn(FnDecl { ident, .. })) => { + bindings_scope.insert(ident.sym.as_ref().into()); + } + Stmt::Decl(Decl::Var(VarDecl { decls, .. })) => { + decls.into_iter().for_each(|decl| match decl { + VarDeclarator { + name: Pat::Ident(ident), + init: Some(init_expr), + .. + } => match init_expr.as_ref() { + // const useFancyState = function () {} + Expr::Fn(_) => { + bindings_scope.insert(ident.sym.as_ref().into()); + } + // const useFancyState = () => {} + Expr::Arrow(_) => { + bindings_scope.insert(ident.sym.as_ref().into()); + } + _ => {} + }, + _ => {} + }); + } + _ => {} + } + }); + + stmts.into_iter().for_each(|stmt| { + match stmt { + // function useFancyState() {} + Stmt::Decl(Decl::Fn(FnDecl { + ident, + function: Function { + body: Some(body), .. + }, + .. + })) => { + if let (_, Some(signature)) = self.get_persistent_fn(&bindings_scope, Some(ident), body) { + exotic_signatures.push((index, signature, None)); + } + } + // var ... + Stmt::Decl(Decl::Var(VarDecl { decls, .. })) => { + decls.into_iter().for_each(|decl| match decl { + VarDeclarator { + name, + init: Some(init_expr), + .. + } => match init_expr.as_mut() { + // const useFancyState = function () {} + Expr::Fn(FnExpr { + function: Function { + body: Some(body), .. + }, + .. + }) => match name { + Pat::Ident(ident) => { + if let (_, Some(signature)) = + self.get_persistent_fn(&bindings_scope, Some(ident), body) + { + exotic_signatures.push((index, signature, None)); + } + } + _ => {} + }, + // const useFancyState = () => {} + Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::BlockStmt(body), + .. + }) => match name { + Pat::Ident(ident) => { + if let (_, Some(signature)) = + self.get_persistent_fn(&bindings_scope, Some(ident), body) + { + exotic_signatures.push((index, signature, None)); + } + } + _ => {} + }, + // cosnt [state, setState] = useSate() + Expr::Call(call) => match self.get_hook_call(Some(name), call) { + Some(hc) => hook_calls.push(hc), + _ => {} + }, + _ => {} + }, + _ => {} + }); + } + // useEffect() + Stmt::Expr(ExprStmt { expr, .. }) => match expr.as_ref() { + Expr::Call(call) => match self.get_hook_call(None, call) { + Some(hc) => hook_calls.push(hc), + _ => {} + }, + _ => {} + }, + // return .. + Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => match arg.as_mut() { + // return function() {} + Expr::Fn(FnExpr { + function: Function { + body: Some(body), .. + }, + .. + }) => { + if let (_, Some(signature)) = self.get_persistent_fn(&bindings_scope, None, body) { + exotic_signatures.push((index, signature, Some(arg.as_ref().clone()))); + } + } + // return () => {} + Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::BlockStmt(body), + .. + }) => { + if let (_, Some(signature)) = self.get_persistent_fn(&bindings_scope, None, body) { + exotic_signatures.push((index, signature, Some(arg.as_ref().clone()))); + } + } + _ => {} + }, + _ => {} + } + index += 1; + }); + + // ! insert + // _s(); + let mut inserted: usize = 0; + let signature = if hook_calls.len() > 0 { + let mut handle_ident = String::from("_s"); + self.signature_index += 1; + if self.signature_index > 1 { + handle_ident.push_str(self.signature_index.to_string().as_str()); + }; + let handle_ident = private_ident!(handle_ident.as_str()); + block_stmt.stmts.insert( + 0, + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(handle_ident.clone()))), + args: vec![], + type_args: None, + })), + }), + ); + inserted += 1; + Some(Signature { + parent_ident: match ident { + Some(ident) => Some(ident.clone()), + None => None, + }, + handle_ident, + hook_calls, + }) + } else { + None + }; + + if exotic_signatures.len() > 0 { + // ! insert + // var _s = $RefreshSig$(), _s2 = $RefreshSig$(); + block_stmt.stmts.insert( + inserted, + Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: exotic_signatures + .clone() + .into_iter() + .map(|signature| VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(signature.1.handle_ident), + init: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!(self + .refresh_sig + .as_str())))), + args: vec![], + type_args: None, + }))), + definite: false, + }) + .collect(), + })), + ); + inserted += 1; + + for (index, exotic_signature, return_expr) in exotic_signatures { + let mut args = self.create_arguments_for_signature(&bindings_scope, &exotic_signature); + if let Some(return_expr) = return_expr { + args.insert( + 0, + ExprOrSpread { + spread: None, + expr: Box::new(return_expr), + }, + ); + block_stmt.stmts[index + inserted] = Stmt::Return(ReturnStmt { + span: DUMMY_SP, + arg: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident( + exotic_signature.handle_ident.clone(), + ))), + args, + type_args: None, + }))), + }); + } else { + block_stmt.stmts.insert( + index + inserted + 1, + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident( + exotic_signature.handle_ident.clone(), + ))), + args, + type_args: None, + })), + }), + ); + inserted += 1 + } + } + } + (fc_id, signature) + } + + fn get_hook_call(&self, pat: Option<&Pat>, call: &CallExpr) -> Option { + if let Some((obj, ident)) = get_call_callee(call) { + let ident_str = ident.sym.as_ref(); + let is_builtin = is_builtin_hook( + match &obj { + Some(obj) => Some(obj), + None => None, + }, + ident_str, + ); + if is_builtin + || (ident_str.len() > 3 + && ident_str.starts_with("use") + && ident_str[3..].starts_with(char::is_uppercase)) + { + let mut key = ident_str.to_owned(); + match pat { + Some(pat) => { + let name = self.source.span_to_snippet(pat.span()).unwrap(); + key.push('{'); + key.push_str(name.as_str()); + // `useState` first argument is initial state. + if call.args.len() > 0 && is_builtin && ident_str == "useState" { + key.push('('); + key.push_str( + self + .source + .span_to_snippet(call.args[0].span()) + .unwrap() + .as_str(), + ); + key.push(')'); + } + // `useReducer` second argument is initial state. + if call.args.len() > 1 && is_builtin && ident_str == "useReducer" { + key.push('('); + key.push_str( + self + .source + .span_to_snippet(call.args[1].span()) + .unwrap() + .as_str(), + ); + key.push(')'); + } + key.push('}'); + } + _ => key.push_str("{}"), + }; + return Some(HookCall { + obj, + ident, + key, + is_builtin, + }); + } + } + None + } + + fn find_inner_component( + &mut self, + bindings: &IndexSet, + parent_name: &str, + call: &mut CallExpr, + ) -> bool { + if !is_componentish_name(parent_name) && !parent_name.starts_with("%default%") { + return false; + } + + if call.args.len() == 0 { + return false; + } + + // first arg should be a function or call + match call.args[0].expr.as_ref() { + Expr::Fn(_) => {} + Expr::Arrow(_) => {} + Expr::Call(_) => {} + _ => return false, + } + + if let Some((obj, ident)) = get_call_callee(call) { + let mut ident_str = parent_name.to_owned(); + ident_str.push('$'); + match obj { + Some(obj) => { + ident_str.push_str(obj.sym.as_ref()); + ident_str.push('.'); + } + _ => {} + } + ident_str.push_str(ident.sym.as_ref()); + match call.args[0].expr.as_mut() { + Expr::Call(inner_call) => { + if let Some(_) = get_call_callee(inner_call) { + let ok = self.find_inner_component(bindings, ident_str.as_str(), inner_call); + if ok { + let handle_ident = self.create_registration_handle_ident(); + self + .registrations + .push((handle_ident.clone(), ident_str.clone())); + call.args[0] = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Expr(Box::new(Expr::Ident(handle_ident))), + right: Box::new(Expr::Call(inner_call.clone())), + })), + } + } + return ok; + } + } + _ => {} + } + + let handle_ident = self.create_registration_handle_ident(); + self.registrations.push((handle_ident.clone(), ident_str)); + match call.args[0].expr.as_mut() { + Expr::Fn(fn_expr) => { + let mut right = Box::new(Expr::Fn(fn_expr.clone())); + match &mut fn_expr.function { + Function { + body: Some(body), .. + } => { + if let (_, Some(signature)) = self.get_persistent_fn(bindings, None, body) { + let mut args = self.create_arguments_for_signature(bindings, &signature); + args.insert( + 0, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Fn(fn_expr.clone())), + }, + ); + right = Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(signature.handle_ident.clone()))), + args, + type_args: None, + })); + self.signatures.push(signature); + } + } + _ => {} + }; + call.args[0] = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Expr(Box::new(Expr::Ident(handle_ident))), + right, + })), + } + } + Expr::Arrow(arrow_expr) => { + let mut right = Box::new(Expr::Arrow(arrow_expr.clone())); + match &mut arrow_expr.body { + BlockStmtOrExpr::BlockStmt(body) => { + if let (_, Some(signature)) = self.get_persistent_fn(bindings, None, body) { + let mut args = self.create_arguments_for_signature(bindings, &signature); + args.insert( + 0, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Arrow(arrow_expr.clone())), + }, + ); + right = Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(signature.handle_ident.clone()))), + args, + type_args: None, + })); + self.signatures.push(signature); + } + } + _ => {} + }; + call.args[0] = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Expr(Box::new(Expr::Ident(handle_ident))), + right, + })), + } + } + _ => {} + } + return true; + } + false + } + + fn create_arguments_for_signature( + &self, + bindings: &IndexSet, + signature: &Signature, + ) -> Vec { + let mut key = Vec::::new(); + let mut custom_hooks_in_scope = Vec::<(Option, Ident)>::new(); + let mut args: Vec = vec![]; + match &signature.parent_ident { + Some(parent_ident) => args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(parent_ident.clone())), + }), + None => {} + } + let mut force_reset = false; + // todo: parse @refresh reset command + signature.hook_calls.clone().into_iter().for_each(|call| { + key.push(call.key); + if !call.is_builtin { + match call.obj { + Some(obj) => { + if bindings.contains(obj.sym.as_ref().into()) { + custom_hooks_in_scope.push((Some(obj.clone()), call.ident.clone())); + } else { + force_reset = true + } + } + None => { + if bindings.contains(call.ident.sym.as_ref().into()) { + custom_hooks_in_scope.push((None, call.ident.clone())); + } else { + force_reset = true; + } + } + } + } + }); + let mut key = key.join("\n"); + if !self.emit_full_signatures { + let mut hasher = Sha1::new(); + hasher.update(key); + key = base64::encode(hasher.finalize()); + } + args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: key.into(), + has_escape: false, + kind: Default::default(), + }))), + }); + if force_reset || custom_hooks_in_scope.len() > 0 { + args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: force_reset, + }))), + }); + } + if custom_hooks_in_scope.len() > 0 { + args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Arrow(ArrowExpr { + span: DUMMY_SP, + params: vec![], + body: BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: custom_hooks_in_scope + .into_iter() + .map(|hook| { + let (obj, id) = hook; + if let Some(obj) = obj { + Some(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(obj.clone()))), + prop: Box::new(Expr::Ident(id.clone())), + computed: false, + })), + }) + } else { + Some(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(id.clone())), + }) + } + }) + .collect(), + }))), + is_async: false, + is_generator: false, + type_params: None, + return_type: None, + })), + }); + } + args + } +} + +impl Fold for FastRefreshFold { + noop_fold_type!(); + + fn fold_module_items(&mut self, module_items: Vec) -> Vec { + let mut items = Vec::::new(); + let mut raw_items = Vec::::new(); + let mut bindings = IndexSet::::new(); + + // collect top bindings + for item in module_items.clone() { + match item { + // import React, {useState} from "/react.js" + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { specifiers, .. })) => { + specifiers + .into_iter() + .for_each(|specifier| match specifier { + ImportSpecifier::Named(ImportNamedSpecifier { local, .. }) + | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) + | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + bindings.insert(local.sym.as_ref().into()); + } + }); + } + + // export function App() {} + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: Decl::Fn(FnDecl { ident, .. }), + .. + })) => { + bindings.insert(ident.sym.as_ref().into()); + } + + // export default function App() {} + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { + decl: DefaultDecl::Fn(FnExpr { + ident: Some(ident), .. + }), + .. + })) => { + bindings.insert(ident.sym.as_ref().into()); + } + + // function App() {} + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))) => { + bindings.insert(ident.sym.as_ref().into()); + } + + // const Foo = () => {} + // export const App = () => {} + ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { decls, .. }))) + | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: Decl::Var(VarDecl { decls, .. }), + .. + })) => { + decls.into_iter().for_each(|decl| match decl { + VarDeclarator { + name: Pat::Ident(ident), + .. + } => { + bindings.insert(ident.sym.as_ref().into()); + } + _ => {} + }); + } + + _ => {} + }; + } + + for mut item in module_items { + let mut persistent_fns = Vec::<(Option, Option)>::new(); + let mut hocs = Vec::<(Ident, Ident)>::new(); + match &mut item { + // export function App() {} + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: + Decl::Fn(FnDecl { + ident, + function: Function { + body: Some(body), .. + }, + .. + }), + .. + })) => persistent_fns.push(self.get_persistent_fn(&bindings, Some(ident), body)), + + // export default function App() {} + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { + decl: + DefaultDecl::Fn(FnExpr { + ident: Some(ident), + function: Function { + body: Some(body), .. + }, + .. + }), + .. + })) => persistent_fns.push(self.get_persistent_fn(&bindings, Some(ident), body)), + + // export default React.memo(() => {}) + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + expr, .. + })) => match expr.as_mut() { + Expr::Call(call) => { + if self.find_inner_component(&bindings, "%default%", call) { + let handle_ident = self.create_registration_handle_ident(); + self + .registrations + .push((handle_ident.clone(), "%default%".into())); + // export default _c2 = React.memo(_c = () => {}) + item = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Expr(Box::new(Expr::Ident(handle_ident))), + right: Box::new(Expr::Call(call.clone())), + })), + })); + } + } + _ => {} + }, + + // function App() {} + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident, + function: Function { + body: Some(body), .. + }, + .. + }))) => persistent_fns.push(self.get_persistent_fn(&bindings, Some(ident), body)), + + // const Foo = () => {} + // export const App = () => {} + ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { decls, .. }))) + | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + decl: Decl::Var(VarDecl { decls, .. }), + .. + })) => { + decls.into_iter().for_each(|decl| match decl { + VarDeclarator { + name: Pat::Ident(ident), + init: Some(init_expr), + .. + } => { + match init_expr.as_mut() { + // const Foo = function () {} + Expr::Fn(FnExpr { + function: Function { + body: Some(body), .. + }, + .. + }) => persistent_fns.push(self.get_persistent_fn(&bindings, Some(ident), body)), + // const Foo = () => {} + Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::BlockStmt(body), + .. + }) => persistent_fns.push(self.get_persistent_fn(&bindings, Some(ident), body)), + // const Bar = () =>
+ Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::Expr(expr), + .. + }) => match expr.as_ref() { + Expr::JSXElement(jsx) => match jsx.as_ref() { + JSXElement { .. } => persistent_fns.push((Some(ident.clone()), None)), + }, + _ => {} + }, + // const A = forwardRef(function() {}); + Expr::Call(call) => { + if self.find_inner_component(&bindings, ident.sym.as_ref(), call) { + let handle_ident = self.create_registration_handle_ident(); + self + .registrations + .push((handle_ident.clone(), ident.sym.as_ref().into())); + hocs.push((ident.clone(), handle_ident)) + } + } + _ => {} + } + } + _ => {} + }); + } + + _ => {} + }; + + raw_items.push(item); + + for (fc_id, signature) in persistent_fns { + if let Some(fc_id) = fc_id { + let registration_handle_id = self.create_registration_handle_ident(); + self + .registrations + .push((registration_handle_id.clone(), fc_id.sym.as_ref().into())); + + // ! insert + // _c = App; + // _c2 = Foo; + raw_items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Pat(Box::new(Pat::Ident(registration_handle_id))), + right: Box::new(Expr::Ident(fc_id)), + })), + }))); + } + + if let Some(signature) = signature { + self.signatures.push(signature); + } + } + + // ! insert (hoc) + // _c = App; + // _c2 = Foo; + for (hoc_handle_id, hoc_id) in hocs { + raw_items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: PatOrExpr::Pat(Box::new(Pat::Ident(hoc_handle_id))), + right: Box::new(Expr::Ident(hoc_id)), + })), + }))); + } + } + + // ! insert + // var _c, _c2; + if self.registrations.len() > 0 { + items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: self + .registrations + .clone() + .into_iter() + .map(|registration| VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(registration.0), + init: None, + definite: false, + }) + .collect(), + })))); + } + + // ! insert + // var _s = $RefreshSig$(), _s2 = $RefreshSig$(); + if self.signatures.len() > 0 { + items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: self + .signatures + .clone() + .into_iter() + .map(|signature| VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(signature.handle_ident), + init: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!(self + .refresh_sig + .as_str())))), + args: vec![], + type_args: None, + }))), + definite: false, + }) + .collect(), + })))); + } + + // ! insert raw items + for item in raw_items { + items.push(item); + } + + // ! insert + // _s(App, "useState{[count, setCount](0)}\nuseEffect{}"); + for signature in &self.signatures { + match signature.parent_ident { + Some(_) => { + let args = self.create_arguments_for_signature(&bindings, &signature); + items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(signature.handle_ident.clone()))), + args, + type_args: None, + })), + }))); + } + None => {} + } + } + + // ! insert + // $RefreshReg$(_c, "App"); + // $RefreshReg$(_c2, "Foo"); + for (registration_id, fc_name) in self.registrations.clone() { + items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!(self + .refresh_reg + .as_str())))), + args: vec![ + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(registration_id)), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: fc_name.into(), + has_escape: false, + kind: Default::default(), + }))), + }, + ], + type_args: None, + })), + }))); + } + items + } +} + +fn is_componentish_name(name: &str) -> bool { + name.starts_with(char::is_uppercase) +} + +fn is_builtin_hook(obj: Option<&Ident>, id: &str) -> bool { + let ok = match id { + "useState" + | "useReducer" + | "useEffect" + | "useLayoutEffect" + | "useMemo" + | "useCallback" + | "useRef" + | "useContext" + | "useImperativeHandle" + | "useDebugValue" => true, + _ => false, + }; + match obj { + Some(obj) => match obj.sym.as_ref() { + "React" => ok, + _ => false, + }, + None => ok, + } +} + +fn get_call_callee(call: &CallExpr) -> Option<(Option, Ident)> { + let callee = match &call.callee { + ExprOrSuper::Super(_) => return None, + ExprOrSuper::Expr(callee) => callee.as_ref(), + }; + + match callee { + // useState() + Expr::Ident(id) => Some((None, id.clone())), + // React.useState() + Expr::Member(expr) => match &expr.obj { + ExprOrSuper::Expr(obj) => match obj.as_ref() { + Expr::Ident(obj) => match expr.prop.as_ref() { + Expr::Ident(prop) => Some((Some(obj.clone()), prop.clone())), + _ => None, + }, + _ => None, + }, + _ => None, + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::swc::ParsedModule; + use std::cmp::min; + use swc_common::Globals; + + fn t(specifier: &str, source: &str, expect: &str) -> bool { + let module = ParsedModule::parse(specifier, source, None).expect("could not parse module"); + let (code, _) = swc_common::GLOBALS.set(&Globals::new(), || { + module + .apply_transform( + fast_refresh_fold( + "$RefreshReg$", + "$RefreshSig$", + true, + module.source_map.clone(), + ), + false, + ) + .expect("could not transpile module") + }); + + if code != expect { + let mut p: usize = 0; + for i in 0..min(code.len(), expect.len()) { + if code.get(i..i + 1) != expect.get(i..i + 1) { + p = i; + break; + } + } + println!( + "{}\x1b[0;31m{}\x1b[0m", + code.get(0..p).unwrap(), + code.get(p..).unwrap() + ); + } + code == expect + } + + #[test] + fn test_fast_refresh() { + let source = r#" + function Hello() { + return

Hi

; + } + Hello = connect(Hello); + const Bar = () => { + return ; + }; + var Baz = () =>
; + export default function App() { + const [foo, setFoo] = useState(0); + const bar = useState(() => 0); + const [state, dispatch] = useReducer(reducer, initialState, init); + React.useEffect(() => {}, []); + return

{foo}

; + } + "#; + let expect = r#"var _c, _c2, _c3, _c4; +var _s = $RefreshSig$(); +function Hello() { + return

Hi

; +} +_c = Hello; +Hello = connect(Hello); +const Bar = ()=>{ + return ; +}; +_c2 = Bar; +var Baz = ()=>
+; +_c3 = Baz; +export default function App() { + _s(); + const [foo, setFoo] = useState(0); + const bar = useState(()=>0 + ); + const [state, dispatch] = useReducer(reducer, initialState, init); + React.useEffect(()=>{ + }, []); + return

{foo}

; +}; +_c4 = App; +_s(App, "useState{[foo, setFoo](0)}\nuseState{bar(() => 0)}\nuseReducer{[state, dispatch](initialState)}\nuseEffect{}"); +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); +$RefreshReg$(_c3, "Baz"); +$RefreshReg$(_c4, "App"); +"#; + assert!(t("/app.jsx", source, expect)); + } + + #[test] + fn test_fast_refresh_custom_hooks() { + let source = r#" + const useFancyEffect = () => { + React.useEffect(() => { }); + }; + function useFancyState() { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + function useFoo() { + const [x] = useBar(1, 2, 3); + useBarEffect(); + } + export default function App() { + const bar = useFancyState(); + return

{bar}

; + } + "#; + let expect = r#"var _c; +var _s = $RefreshSig$(), _s2 = $RefreshSig$(), _s3 = $RefreshSig$(), _s4 = $RefreshSig$(); +const useFancyEffect = ()=>{ + _s(); + React.useEffect(()=>{ + }); +}; +function useFancyState() { + _s2(); + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; +} +function useFoo() { + _s3(); + const [x] = useBar(1, 2, 3); + useBarEffect(); +} +export default function App() { + _s4(); + const bar = useFancyState(); + return

{bar}

; +}; +_c = App; +_s(useFancyEffect, "useEffect{}"); +_s2(useFancyState, "useState{[foo, setFoo](0)}\nuseFancyEffect{}", false, ()=>[ + useFancyEffect + ] +); +_s3(useFoo, "useBar{[x]}\nuseBarEffect{}", true); +_s4(App, "useFancyState{bar}", false, ()=>[ + useFancyState + ] +); +$RefreshReg$(_c, "App"); +"#; + assert!(t("/app.jsx", source, expect)); + } + + #[test] + fn test_fast_refresh_exotic_signature() { + let source = r#" + import FancyHook from 'fancy'; + + export default function App() { + const useFancyState = () => { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + useFancyEffect(); + + function useFancyEffect() { + useEffect(); + } + + return

{bar}{baz}

; + } + "#; + let expect = r#"var _c; +var _s3 = $RefreshSig$(); +import FancyHook from 'fancy'; +export default function App() { + _s3(); + var _s = $RefreshSig$(), _s2 = $RefreshSig$(); + const useFancyState = ()=>{ + _s(); + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + }; + _s(useFancyState, "useState{[foo, setFoo](0)}\nuseFancyEffect{}", false, ()=>[ + useFancyEffect + ] + ); + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + useFancyEffect(); + function useFancyEffect() { + _s2(); + useEffect(); + } + _s2(useFancyEffect, "useEffect{}"); + return

{bar}{baz}

; +}; +_c = App; +_s3(App, "useFancyState{bar}\nuseThing{baz}\nuseState{}\nuseThePlatform{}\nuseFancyEffect{}", true, ()=>[ + FancyHook.useThing + ] +); +$RefreshReg$(_c, "App"); +"#; + assert!(t("/app.jsx", source, expect)); + } + + #[test] + fn test_fast_refresh_hocs() { + let source = r#" + const A = forwardRef(function() { + return

Foo

; + }); + const B = memo(React.forwardRef(() => { + return

Foo

; + })); + const C = forwardRef(memo(forwardRef(()=>null))) + export const D = React.memo(React.forwardRef((props, ref) => { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; + })); + export const E = React.memo(React.forwardRef(function(props, ref) { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; + })); + function hoc() { + return function Inner() { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; + }; + } + const F = memo('Foo'); + const G = forwardRef(memo(forwardRef())); + const I = forwardRef(memo(forwardRef(0, () => {}))); + export let H = hoc(); + export default React.memo(forwardRef((props, ref) => { + return

Foo

; + })); + "#; + let expect = r#"var _c, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _c10, _c11, _c12, _c13, _c14, _c15, _c16, _c17, _c18; +var _s = $RefreshSig$(), _s2 = $RefreshSig$(); +const A = forwardRef(_c = function() { + return

Foo

; +}); +A = _c2; +const B = memo(_c4 = React.forwardRef(_c3 = ()=>{ + return

Foo

; +})); +B = _c5; +const C = forwardRef(_c8 = memo(_c7 = forwardRef(_c6 = ()=>null +))); +C = _c9; +export const D = React.memo(_c11 = React.forwardRef(_c10 = _s((props, ref)=>{ + _s(); + const [foo, setFoo] = useState(0); + React.useEffect(()=>{ + }); + return

{foo}

; +}, "useState{[foo, setFoo](0)}\nuseEffect{}"))); +D = _c12; +export const E = React.memo(_c14 = React.forwardRef(_c13 = _s2(function(props, ref) { + _s2(); + const [foo, setFoo] = useState(0); + React.useEffect(()=>{ + }); + return

{foo}

; +}, "useState{[foo, setFoo](0)}\nuseEffect{}"))); +E = _c15; +function hoc() { + var _s3 = $RefreshSig$(); + return _s3(function Inner() { + _s3(); + const [foo, setFoo] = useState(0); + React.useEffect(()=>{ + }); + return

{foo}

; + }, "useState{[foo, setFoo](0)}\nuseEffect{}"); +} +const F = memo('Foo'); +const G = forwardRef(memo(forwardRef())); +const I = forwardRef(memo(forwardRef(0, ()=>{ +}))); +export let H = hoc(); +export default _c18 = React.memo(_c17 = forwardRef(_c16 = (props, ref)=>{ + return

Foo

; +})); +$RefreshReg$(_c, "A$forwardRef"); +$RefreshReg$(_c2, "A"); +$RefreshReg$(_c3, "B$memo$React.forwardRef"); +$RefreshReg$(_c4, "B$memo"); +$RefreshReg$(_c5, "B"); +$RefreshReg$(_c6, "C$forwardRef$memo$forwardRef"); +$RefreshReg$(_c7, "C$forwardRef$memo"); +$RefreshReg$(_c8, "C$forwardRef"); +$RefreshReg$(_c9, "C"); +$RefreshReg$(_c10, "D$React.memo$React.forwardRef"); +$RefreshReg$(_c11, "D$React.memo"); +$RefreshReg$(_c12, "D"); +$RefreshReg$(_c13, "E$React.memo$React.forwardRef"); +$RefreshReg$(_c14, "E$React.memo"); +$RefreshReg$(_c15, "E"); +$RefreshReg$(_c16, "%default%$React.memo$forwardRef"); +$RefreshReg$(_c17, "%default%$React.memo"); +$RefreshReg$(_c18, "%default%"); +"#; + assert!(t("/app.jsx", source, expect)); + } + + #[test] + fn test_fast_refresh_ignored() { + let source = r#" + const NotAComp = 'hi'; + export { Baz, NotAComp }; + export function sum() {} + export const Bad = 42; + + let connect = () => { + function Comp() { + const handleClick = () => {}; + return

Hi

; + } + return Comp; + }; + function withRouter() { + return function Child() { + const handleClick = () => {}; + return

Hi

; + } + }; + + let A = foo ? () => { + return

Hi

; + } : null; + const B = (function Foo() { + return

Hi

; + })(); + let C = () => () => { + return

Hi

; + }; + let D = bar && (() => { + return

Hi

; + }); + + const throttledAlert = throttle(function () { + alert('Hi'); + }); + const TooComplex = function () { + return hello; + }(() => {}); + if (cond) { + const Foo = thing(() => {}); + } + + export default function() {} + "#; + let expect = r#"const NotAComp = 'hi'; +export { Baz, NotAComp }; +export function sum() { +} +export const Bad = 42; +let connect = ()=>{ + function Comp() { + const handleClick = ()=>{ + }; + return

Hi

; + } + return Comp; +}; +function withRouter() { + return function Child() { + const handleClick = ()=>{ + }; + return

Hi

; + }; +} +; +let A = foo ? ()=>{ + return

Hi

; +} : null; +const B = (function Foo() { + return

Hi

; +})(); +let C = ()=>()=>{ + return

Hi

; + } +; +let D = bar && (()=>{ + return

Hi

; +}); +const throttledAlert = throttle(function() { + alert('Hi'); +}); +const TooComplex = function() { + return hello; +}(()=>{ +}); +if (cond) { + const Foo = thing(()=>{ + }); +} +export default function() { +}; +"#; + assert!(t("/app.jsx", source, expect)); + } +} diff --git a/compiler/src/import_map.rs b/compiler/src/import_map.rs new file mode 100644 index 000000000..bd04dfe5a --- /dev/null +++ b/compiler/src/import_map.rs @@ -0,0 +1,136 @@ +// Copyright 2020 the Aleph.js authors. All rights reserved. MIT license. + +use indexmap::IndexMap; +use serde::Deserialize; +use std::collections::HashMap; + +type SpecifierHashMap = HashMap>; +type SpecifierMap = IndexMap>; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ImportHashMap { + #[serde(default)] + pub imports: SpecifierHashMap, + #[serde(default)] + pub scopes: HashMap, +} + +impl Default for ImportHashMap { + fn default() -> Self { + ImportHashMap { + imports: HashMap::new(), + scopes: HashMap::new(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportMap { + pub imports: SpecifierMap, + pub scopes: IndexMap, +} + +impl ImportMap { + pub fn from_hashmap(map: ImportHashMap) -> Self { + let mut imports: SpecifierMap = IndexMap::new(); + let mut scopes = IndexMap::new(); + for (k, v) in map.imports.iter() { + imports.insert(k.into(), v.to_vec()); + } + for (k, v) in map.scopes.iter() { + let mut imports_: SpecifierMap = IndexMap::new(); + for (k_, v_) in v.iter() { + imports_.insert(k_.into(), v_.to_vec()); + } + scopes.insert(k.into(), imports_); + } + ImportMap { imports, scopes } + } + + pub fn resolve(&self, specifier: &str, url: &str) -> String { + for (prefix, scope_imports) in self.scopes.iter() { + if prefix.ends_with("/") && specifier.starts_with(prefix) { + match scope_imports.get(url) { + Some(alias) => { + let n = alias.len(); + if n > 0 { + return alias[n - 1].to_owned(); + } + } + _ => {} + }; + for (k, alias) in scope_imports.iter() { + if k.ends_with("/") && url.starts_with(k) { + let n = alias.len(); + if n > 0 { + let mut alias = alias[n - 1].to_owned(); + alias.push_str(url[k.len()..].into()); + return alias; + } + } + } + } + } + match self.imports.get(url) { + Some(alias) => { + let n = alias.len(); + if n > 0 { + return alias[n - 1].to_owned(); + } + } + _ => {} + }; + for (k, alias) in self.imports.iter() { + if k.ends_with("/") && url.starts_with(k) { + let n = alias.len(); + if n > 0 { + let mut alias = alias[n - 1].to_owned(); + alias.push_str(url[k.len()..].into()); + return alias; + } + } + } + url.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_import_maps() { + let mut imports: SpecifierHashMap = HashMap::new(); + let mut scopes: HashMap = HashMap::new(); + let mut scope_imports: SpecifierHashMap = HashMap::new(); + imports.insert("react".into(), vec!["https://esm.sh/react".into()]); + imports.insert( + "react-dom/".into(), + vec!["https://esm.sh/react-dom/".into()], + ); + imports.insert( + "https://deno.land/x/aleph/".into(), + vec!["http://localhost:9006/".into()], + ); + scope_imports.insert("react".into(), vec!["https://esm.sh/react@16.4.0".into()]); + scopes.insert("/scope/".into(), scope_imports); + let import_map = ImportMap::from_hashmap(ImportHashMap { imports, scopes }); + assert_eq!( + import_map.resolve("./app.tsx", "react"), + "https://esm.sh/react" + ); + assert_eq!( + import_map.resolve("./app.tsx", "https://deno.land/x/aleph/mod.ts"), + "http://localhost:9006/mod.ts" + ); + assert_eq!( + import_map.resolve("./renderer.ts", "react-dom/server"), + "https://esm.sh/react-dom/server" + ); + assert_eq!( + import_map.resolve("/scope/react-dom", "react"), + "https://esm.sh/react@16.4.0" + ); + } +} diff --git a/compiler/src/jsx.rs b/compiler/src/jsx.rs new file mode 100644 index 000000000..e9f8808a7 --- /dev/null +++ b/compiler/src/jsx.rs @@ -0,0 +1,482 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use crate::aleph::VERSION; +use crate::resolve::{ + create_aleph_pack_var_decl, is_remote_url, DependencyDescriptor, InlineStyle, Resolver, +}; + +use path_slash::PathBufExt; +use sha1::{Digest, Sha1}; +use std::{cell::RefCell, path::PathBuf, rc::Rc}; +use swc_common::{SourceMap, Spanned, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_utils::quote_ident; +use swc_ecma_visit::{noop_fold_type, Fold, FoldWith}; + +pub fn aleph_jsx_fold( + resolver: Rc>, + source: Rc, + is_dev: bool, +) -> (impl Fold, impl Fold) { + ( + AlephJsxFold { + resolver: resolver.clone(), + source, + inline_style_idx: 0, + is_dev, + }, + AlephJsxBuiltinModuleResolveFold { + resolver: resolver.clone(), + }, + ) +} + +/// aleph.js jsx fold, core functions include: +/// - add `__sourceFile` prop in development mode +/// - resolve `a` to `Anchor` +/// - resolve `head` to `Head` +/// - resolve `link` to `Link` +/// - resolve `Link` component `href` prop +/// - resolve `script` to `Script` +/// - resolve `style` to `Style` +/// - optimize `img` in producation mode +struct AlephJsxFold { + resolver: Rc>, + source: Rc, + inline_style_idx: i32, + is_dev: bool, +} + +impl AlephJsxFold { + fn new_inline_style_ident(&mut self) -> String { + let resolver = self.resolver.borrow_mut(); + self.inline_style_idx = self.inline_style_idx + 1; + let path = format!("{}-{}", resolver.specifier, self.inline_style_idx); + let mut ident: String = "inline-style-".to_owned(); + let mut hasher = Sha1::new(); + hasher.update(path.as_bytes()); + let hash = hasher.finalize(); + ident.push_str(format!("{:x}", hash).as_str()); + ident + } + + fn fold_jsx_opening_element( + &mut self, + mut el: JSXOpeningElement, + ) -> (JSXOpeningElement, Option<(String, String)>) { + let mut inline_style: Option<(String, String)> = None; + + match &el.name { + JSXElementName::Ident(id) => { + let name = id.sym.as_ref(); + match name { + "head" | "script" => { + let mut resolver = self.resolver.borrow_mut(); + resolver.builtin_jsx_tags.insert(name.into()); + el.name = JSXElementName::Ident(quote_ident!(rename_builtin_tag(name))); + } + + "a" => { + let mut resolver = self.resolver.borrow_mut(); + let mut should_replace = true; + + for attr in &el.attrs { + match &attr { + JSXAttrOrSpread::JSXAttr(JSXAttr { + name: JSXAttrName::Ident(id), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { value, .. }))), + .. + }) => { + let key = id.sym.as_ref(); + let value = value.as_ref(); + if (key == "href" && is_remote_url(value)) + || (key == "target" && value == "_blank") + { + should_replace = false + } + } + _ => {} + }; + } + + if should_replace { + resolver.builtin_jsx_tags.insert(name.into()); + el.name = JSXElementName::Ident(quote_ident!(rename_builtin_tag(name))); + } + } + + "link" | "Link" => { + let mut should_replace = false; + + for attr in &el.attrs { + match &attr { + JSXAttrOrSpread::JSXAttr(JSXAttr { + name: JSXAttrName::Ident(id), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { value, .. }))), + .. + }) => { + let key = id.sym.as_ref(); + let value = value.as_ref(); + if key == "rel" + && (value == "stylesheet" + || value == "style" + || value == "component") + { + should_replace = true + } + } + _ => {} + }; + } + + if should_replace { + let mut href_prop_index: i32 = -1; + let mut base_prop_index: i32 = -1; + let mut url_prop_index: i32 = -1; + let mut href_prop_value = ""; + + for (i, attr) in el.attrs.iter().enumerate() { + match &attr { + JSXAttrOrSpread::JSXAttr(JSXAttr { + name: JSXAttrName::Ident(id), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { value, .. }))), + .. + }) => match id.sym.as_ref() { + "href" => { + href_prop_index = i as i32; + href_prop_value = value.as_ref(); + } + "__base" => { + base_prop_index = i as i32; + } + "__url" => { + url_prop_index = i as i32; + } + _ => {} + }, + _ => continue, + }; + } + + let mut resolver = self.resolver.borrow_mut(); + let (resolved_path, fixed_url) = + resolver.resolve(href_prop_value, true); + + if href_prop_index >= 0 { + el.attrs[href_prop_index as usize] = + JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(quote_ident!("href")), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: resolved_path.into(), + has_escape: false, + kind: Default::default(), + }))), + }); + } + + let mut buf = PathBuf::from( + resolver + .fix_import_url(resolver.specifier.as_str()) + .as_str(), + ); + buf.pop(); + let base_attr = JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(quote_ident!("__base")), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: buf.to_slash().unwrap().into(), + has_escape: false, + kind: Default::default(), + }))), + }); + let url_attr = JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(quote_ident!("__url")), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: fixed_url.into(), + has_escape: false, + kind: Default::default(), + }))), + }); + if base_prop_index >= 0 { + el.attrs[base_prop_index as usize] = base_attr; + } else { + el.attrs.push(base_attr); + } + if url_prop_index >= 0 { + el.attrs[url_prop_index as usize] = url_attr; + } else { + el.attrs.push(url_attr); + } + + if name.eq("link") { + resolver.builtin_jsx_tags.insert(name.into()); + el.name = + JSXElementName::Ident(quote_ident!(rename_builtin_tag(name))); + } + } + } + + "style" => { + let mut id_prop_index: i32 = -1; + let mut type_prop_value = "css".to_owned(); + + for (i, attr) in el.attrs.iter().enumerate() { + match &attr { + JSXAttrOrSpread::JSXAttr(JSXAttr { + name: JSXAttrName::Ident(id), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { value, .. }))), + .. + }) => match id.sym.as_ref() { + "__styleId" => { + id_prop_index = i as i32; + } + "type" => { + type_prop_value = + value.as_ref().trim_start_matches("text/").to_string(); + } + _ => {} + }, + _ => continue, + }; + } + + let id = self.new_inline_style_ident(); + let id_attr = JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(quote_ident!("__styleId")), + value: Some(JSXAttrValue::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: id.clone().into(), + has_escape: false, + kind: Default::default(), + }))), + }); + if id_prop_index >= 0 { + el.attrs[id_prop_index as usize] = id_attr; + } else { + el.attrs.push(id_attr); + } + + let mut resolver = self.resolver.borrow_mut(); + resolver.dep_graph.push(DependencyDescriptor { + specifier: "#".to_owned() + id.as_str(), + is_dynamic: false, + }); + resolver.builtin_jsx_tags.insert(name.into()); + el.name = JSXElementName::Ident(quote_ident!(rename_builtin_tag(name))); + inline_style = Some((type_prop_value, id.into())); + } + + "img" => { + //todo: optimize img + } + + _ => {} + } + } + _ => {} + }; + + // copy from https://github.com/swc-project/swc/blob/master/ecmascript/transforms/src/react/jsx_src.rs + if self.is_dev { + let resolver = self.resolver.borrow_mut(); + match self.source.span_to_lines(el.span) { + Ok(file_lines) => { + el.attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr { + span: DUMMY_SP, + name: JSXAttrName::Ident(quote_ident!("__source")), + value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer { + span: DUMMY_SP, + expr: JSXExpr::Expr(Box::new( + ObjectLit { + span: DUMMY_SP, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::KeyValue( + KeyValueProp { + key: PropName::Ident(quote_ident!("fileName")), + value: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: resolver.specifier.as_str().into(), + has_escape: false, + kind: Default::default(), + }))), + }, + ))), + PropOrSpread::Prop(Box::new(Prop::KeyValue( + KeyValueProp { + key: PropName::Ident(quote_ident!("lineNumber")), + value: Box::new(Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: (file_lines.lines[0].line_index + 1) + as _, + }))), + }, + ))), + ], + } + .into(), + )), + })), + })); + } + _ => {} + }; + } + + (el, inline_style) + } +} + +impl Fold for AlephJsxFold { + noop_fold_type!(); + + fn fold_jsx_element(&mut self, mut el: JSXElement) -> JSXElement { + if el.span == DUMMY_SP { + return el; + } + + let mut children: Vec = vec![]; + let (opening, inline_style) = self.fold_jsx_opening_element(el.opening); + match inline_style { + Some(ref inline_style) => { + if el.children.len() == 1 { + match el.children.first().unwrap() { + JSXElementChild::JSXExprContainer(JSXExprContainer { + expr: JSXExpr::Expr(expr), + .. + }) => match expr.as_ref() { + Expr::Tpl(Tpl { exprs, quasis, .. }) => { + let mut resolver = self.resolver.borrow_mut(); + let mut es: Vec = vec![]; + let mut qs: Vec = vec![]; + for expr in exprs { + let raw = self + .source + .span_to_snippet(expr.as_ref().span().clone()) + .unwrap(); + es.push(raw.into()); + } + for quasi in quasis { + let raw = + self.source.span_to_snippet(quasi.span.clone()).unwrap(); + qs.push(raw.into()); + } + let (t, id) = inline_style; + resolver.inline_styles.insert( + id.into(), + InlineStyle { + r#type: t.into(), + exprs: es, + quasis: qs, + }, + ); + el.children = + vec![JSXElementChild::JSXExprContainer(JSXExprContainer { + span: DUMMY_SP, + expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: format!("%%{}-placeholder%%", id).into(), + has_escape: false, + kind: Default::default(), + })))), + })]; + } + _ => {} + }, + _ => {} + } + } + } + _ => {} + } + for child in el.children { + children.push(child.fold_children_with(self)); + } + JSXElement { + span: DUMMY_SP, + opening, + children, + ..el + } + } +} + +/// aleph.js jsx builtin module resolve fold. +struct AlephJsxBuiltinModuleResolveFold { + resolver: Rc>, +} + +impl Fold for AlephJsxBuiltinModuleResolveFold { + noop_fold_type!(); + + fn fold_module_items(&mut self, module_items: Vec) -> Vec { + let mut items = Vec::::new(); + let mut resolver = self.resolver.borrow_mut(); + + for mut name in resolver.builtin_jsx_tags.clone() { + if name.eq("a") { + name = "anchor".to_owned() + } + let id = quote_ident!(rename_builtin_tag(name.as_str())); + let (resolved_path, fixed_url) = resolver.resolve( + format!( + "https://deno.land/x/aleph@v{}/framework/react/{}.ts", + VERSION.as_str(), + name + ) + .as_str(), + false, + ); + if resolver.bundle_mode { + items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: vec![create_aleph_pack_var_decl( + id, + fixed_url.as_str(), + Some("default"), + )], + })))); + } else { + items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { + span: DUMMY_SP, + local: id, + })], + src: Str { + span: DUMMY_SP, + value: resolved_path.into(), + has_escape: false, + kind: Default::default(), + }, + type_only: false, + asserts: None, + }))); + } + } + + for item in module_items { + items.push(item) + } + items + } +} + +fn rename_builtin_tag(name: &str) -> String { + let mut c = name.chars(); + let mut name = match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + }; + if name.eq("A") { + name = "Anchor".into(); + } + "__ALEPH_".to_owned() + name.as_str() +} diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs new file mode 100644 index 000000000..f17256fba --- /dev/null +++ b/compiler/src/lib.rs @@ -0,0 +1,151 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +#[macro_use] +extern crate lazy_static; + +mod aleph; +mod compat_fixer; +mod error; +mod fast_refresh; +mod import_map; +mod jsx; +mod resolve; +mod source_type; +mod swc; + +use import_map::ImportHashMap; +use resolve::{DependencyDescriptor, InlineStyle, Resolver}; +use serde::{Deserialize, Serialize}; +use source_type::SourceType; +use std::collections::HashMap; +use std::{cell::RefCell, rc::Rc}; +use swc::{EmitOptions, ParsedModule}; +use swc_ecmascript::parser::JscTarget; +use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct Options { + pub url: String, + + #[serde(default)] + pub import_map: ImportHashMap, + + #[serde(default = "default_react_version")] + pub react_version: String, + + #[serde(default)] + pub swc_options: SWCOptions, + + #[serde(default)] + pub is_dev: bool, + + #[serde(default)] + pub bundle_mode: bool, + + #[serde(default)] + pub bundled_paths: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct SWCOptions { + #[serde(default = "default_target")] + pub target: JscTarget, + + #[serde(default = "default_pragma")] + pub jsx_factory: String, + + #[serde(default = "default_pragma_frag")] + pub jsx_fragment_factory: String, + + #[serde(default)] + pub source_type: String, + + #[serde(default)] + pub source_map: bool, +} + +impl Default for SWCOptions { + fn default() -> Self { + SWCOptions { + target: default_target(), + jsx_factory: default_pragma(), + jsx_fragment_factory: default_pragma_frag(), + source_type: "".into(), + source_map: false, + } + } +} + +fn default_target() -> JscTarget { + JscTarget::Es2020 +} + +fn default_pragma() -> String { + "React.createElement".into() +} + +fn default_pragma_frag() -> String { + "React.Fragment".into() +} + +fn default_react_version() -> String { + "17.0.1".into() +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransformOutput { + pub code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub map: Option, + pub deps: Vec, + pub inline_styles: HashMap, +} + +#[wasm_bindgen(js_name = "transformSync")] +pub fn transform_sync(s: &str, opts: JsValue) -> Result { + console_error_panic_hook::set_once(); + + let opts: Options = opts + .into_serde() + .map_err(|err| format!("failed to parse options: {}", err)) + .unwrap(); + let resolver = Rc::new(RefCell::new(Resolver::new( + opts.url.as_str(), + opts.import_map, + Some(opts.react_version), + opts.bundle_mode, + opts.bundled_paths, + ))); + let specify_source_type = match opts.swc_options.source_type.as_str() { + "js" => Some(SourceType::JavaScript), + "jsx" => Some(SourceType::JSX), + "ts" => Some(SourceType::TypeScript), + "tsx" => Some(SourceType::TSX), + _ => None, + }; + let module = ParsedModule::parse(opts.url.as_str(), s, specify_source_type) + .expect("could not parse module"); + let (code, map) = module + .transpile( + resolver.clone(), + &EmitOptions { + target: opts.swc_options.target, + jsx_factory: opts.swc_options.jsx_factory.clone(), + jsx_fragment_factory: opts.swc_options.jsx_fragment_factory.clone(), + source_map: opts.swc_options.source_map, + is_dev: opts.is_dev, + }, + ) + .expect("could not transpile module"); + let r = resolver.borrow_mut(); + Ok(JsValue::from_serde(&TransformOutput { + code, + map, + deps: r.dep_graph.clone(), + inline_styles: r.inline_styles.clone(), + }) + .unwrap()) +} diff --git a/compiler/src/resolve.rs b/compiler/src/resolve.rs new file mode 100644 index 000000000..ef8c0f999 --- /dev/null +++ b/compiler/src/resolve.rs @@ -0,0 +1,1002 @@ +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use crate::aleph::VERSION; +use crate::import_map::{ImportHashMap, ImportMap}; + +use indexmap::IndexSet; +use path_slash::PathBufExt; +use pathdiff::diff_paths; +use regex::Regex; +use relative_path::RelativePath; +use serde::Serialize; +use sha1::{Digest, Sha1}; +use std::{ + cell::RefCell, + collections::HashMap, + path::{Path, PathBuf}, + rc::Rc, + str::FromStr, +}; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_utils::quote_ident; +use swc_ecma_visit::{noop_fold_type, Fold, FoldWith}; +use url::Url; + +lazy_static! { + pub static ref HASH_PLACEHOLDER: String = "x".repeat(9); + pub static ref RE_ENDS_WITH_VERSION: Regex = Regex::new( + r"@\d+(\.\d+){0,2}(\-[a-z0-9]+(\.[a-z0-9]+)?)?$" + ) + .unwrap(); + pub static ref RE_REACT_URL: Regex = Regex::new( + r"^https?://(esm.sh/|cdn.esm.sh/v\d+/|esm.x-static.io/v\d+/|jspm.dev/|cdn.skypack.dev/|jspm.dev/npm:|esm.run/)react(\-dom)?(@[\^|~]{0,1}[0-9a-z\.\-]+)?([/|\?].*)?$" + ) + .unwrap(); +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DependencyDescriptor { + /// The text specifier associated with the import/export statement. + pub specifier: String, + /// A flag indicating if the import is dynamic or not. + pub is_dynamic: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineStyle { + pub r#type: String, + pub quasis: Vec, + pub exprs: Vec, +} + +/// A Resolver to resolve aleph.js import/export URL. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Resolver { + /// The text specifier associated with the import/export statement. + pub specifier: String, + /// A flag indicating if the specifier is remote url or not. + pub specifier_is_remote: bool, + /// builtin jsx tags like `a`, `link`, `head`, etc + pub builtin_jsx_tags: IndexSet, + /// dependency graph + pub dep_graph: Vec, + /// inline styles + pub inline_styles: HashMap, + /// bundle mode + pub bundle_mode: bool, + bundled_paths: IndexSet, + import_map: ImportMap, + react_version: Option, +} + +impl Resolver { + pub fn new( + specifier: &str, + import_map: ImportHashMap, + react_version: Option, + bundle_mode: bool, + bundled_paths: Vec, + ) -> Self { + let mut set = IndexSet::::new(); + for url in bundled_paths { + set.insert(url); + } + Resolver { + specifier: specifier.into(), + specifier_is_remote: is_remote_url(specifier), + builtin_jsx_tags: IndexSet::new(), + dep_graph: Vec::new(), + inline_styles: HashMap::new(), + import_map: ImportMap::from_hashmap(import_map), + react_version, + bundle_mode, + bundled_paths: set, + } + } + + /// fix import/export url. + // - `https://esm.sh/react` -> `/-/esm.sh/react.js` + // - `https://esm.sh/react@17.0.1?target=es2015&dev` -> `/-/esm.sh/react@17.0.1_target=es2015&dev.js` + // - `http://localhost:8080/mod` -> `/-/http_localhost_8080/mod.js` + // - `/components/logo.tsx` -> `/components/logo.tsx` + // - `@/components/logo.tsx` -> `/components/logo.tsx` + // - `~/components/logo.tsx` -> `/components/logo.tsx` + // - `../components/logo.tsx` -> `../components/logo.tsx` + // - `./button.tsx` -> `./button.tsx` + // - `/components/foo/./logo.tsx` -> `/components/foo/logo.tsx` + // - `/components/foo/../logo.tsx` -> `/components/logo.tsx` + pub fn fix_import_url(&self, url: &str) -> String { + let is_remote = is_remote_url(url); + if !is_remote { + let mut url = url; + let mut root = Path::new(""); + if url.starts_with("./") { + url = url.trim_start_matches("."); + root = Path::new("."); + } else if url.starts_with("../") { + url = url.trim_start_matches(".."); + root = Path::new(".."); + } else if url.starts_with("@/") { + url = url.trim_start_matches("@"); + } else if url.starts_with("~/") { + url = url.trim_start_matches("~"); + } + return RelativePath::new(url) + .normalize() + .to_path(root) + .to_slash() + .unwrap() + .to_owned(); + } + let url = Url::from_str(url).unwrap(); + let path = Path::new(url.path()); + let mut path_buf = path.to_owned(); + let mut ext = ".".to_owned(); + ext.push_str(match path.extension() { + Some(os_str) => match os_str.to_str() { + Some(s) => { + if RE_ENDS_WITH_VERSION.is_match(url.path()) { + "js" + } else { + s + } + } + None => "js", + }, + None => "js", + }); + match path.file_name() { + Some(os_str) => match os_str.to_str() { + Some(s) => { + let mut file_name = s.trim_end_matches(ext.as_str()).to_owned(); + match url.query() { + Some(q) => { + file_name.push('_'); + file_name.push_str(q); + } + _ => {} + }; + file_name.push_str(ext.as_str()); + path_buf.set_file_name(file_name); + } + _ => {} + }, + _ => {} + }; + let mut p = "/-/".to_owned(); + if url.scheme() == "http" { + p.push_str("http_"); + } + p.push_str(url.host_str().unwrap()); + match url.port() { + Some(port) => { + p.push('_'); + p.push_str(port.to_string().as_str()); + } + _ => {} + } + p.push_str(path_buf.to_str().unwrap()); + p + } + + /// resolve import/export url. + // [/pages/index.tsx] + // - `https://esm.sh/swr` -> `/-/esm.sh/swr.js` + // - `https://esm.sh/react` -> `/-/esm.sh/react@${REACT_VERSION}.js` + // - `https://deno.land/x/aleph/mod.ts` -> `/-/deno.land/x/aleph@v${CURRENT_ALEPH_VERSION}/mod.ts` + // - `../components/logo.tsx` -> `/components/logo.{HASH}.js` + // - `../styles/app.css` -> `/styles/app.css.{HASH}.js` + // - `@/components/logo.tsx` -> `/components/logo.{HASH}.js` + // - `~/components/logo.tsx` -> `/components/logo.{HASH}.js` + pub fn resolve(&mut self, url: &str, is_dynamic: bool) -> (String, String) { + // apply import map + let url = self.import_map.resolve(self.specifier.as_str(), url); + let mut fixed_url: String = if is_remote_url(url.as_str()) { + url.into() + } else { + if self.specifier_is_remote { + let mut new_url = Url::from_str(self.specifier.as_str()).unwrap(); + if url.starts_with("/") { + new_url.set_path(url.as_str()); + } else { + let mut buf = PathBuf::from(new_url.path()); + buf.pop(); + buf.push(url); + let path = "/".to_owned() + + RelativePath::new(buf.to_slash().unwrap().as_str()) + .normalize() + .as_str(); + new_url.set_path(path.as_str()); + } + new_url.as_str().into() + } else { + if url.starts_with("/") { + url.into() + } else if url.starts_with("@/") { + url.trim_start_matches("@").into() + } else if url.starts_with("~/") { + url.trim_start_matches("~").into() + } else { + let mut buf = PathBuf::from(self.specifier.as_str()); + buf.pop(); + buf.push(url); + "/".to_owned() + + RelativePath::new(buf.to_slash().unwrap().as_str()) + .normalize() + .as_str() + } + } + }; + // fix deno.land/x/aleph url + if fixed_url.starts_with("https://deno.land/x/aleph/") { + fixed_url = format!( + "https://deno.land/x/aleph@v{}/{}", + VERSION.as_str(), + fixed_url.trim_start_matches("https://deno.land/x/aleph/") + ); + } + // fix react/react-dom url + if let Some(version) = &self.react_version { + if RE_REACT_URL.is_match(fixed_url.as_str()) { + let caps = RE_REACT_URL.captures(fixed_url.as_str()).unwrap(); + let mut host = caps.get(1).map_or("", |m| m.as_str()); + let non_esm_sh_cdn = !host.starts_with("esm.sh/") + && !host.starts_with("cdn.esm.sh/") + && !host.starts_with("esm.x-static.io/"); + if non_esm_sh_cdn { + host = "esm.sh/" + } + let pkg = caps.get(2).map_or("", |m| m.as_str()); + let ver = caps.get(3).map_or("", |m| m.as_str()); + let path = caps.get(4).map_or("", |m| m.as_str()); + if non_esm_sh_cdn || ver != version { + fixed_url = format!("https://{}react{}@{}{}", host, pkg, version, path); + } + } + } + let is_remote = is_remote_url(fixed_url.as_str()); + let mut resolved_path = if is_remote { + if self.specifier_is_remote { + let mut buf = PathBuf::from(self.fix_import_url(self.specifier.as_str())); + buf.pop(); + diff_paths( + self.fix_import_url(fixed_url.as_str()), + buf.to_slash().unwrap(), + ) + .unwrap() + } else { + let mut buf = PathBuf::from(self.specifier.as_str()); + buf.pop(); + diff_paths( + self.fix_import_url(fixed_url.as_str()), + buf.to_slash().unwrap(), + ) + .unwrap() + } + } else { + if self.specifier_is_remote { + let mut new_url = Url::from_str(self.specifier.as_str()).unwrap(); + if fixed_url.starts_with("/") { + new_url.set_path(fixed_url.as_str()); + } else { + let mut buf = PathBuf::from(new_url.path()); + buf.pop(); + buf.push(fixed_url.as_str()); + let path = "/".to_owned() + + RelativePath::new(buf.to_slash().unwrap().as_str()) + .normalize() + .as_str(); + new_url.set_path(path.as_str()); + } + let mut buf = PathBuf::from(self.fix_import_url(self.specifier.as_str())); + buf.pop(); + diff_paths( + self.fix_import_url(new_url.as_str()), + buf.to_slash().unwrap(), + ) + .unwrap() + } else { + if fixed_url.starts_with("/") { + let mut buf = PathBuf::from(self.specifier.as_str()); + buf.pop(); + diff_paths(fixed_url.clone(), buf.to_slash().unwrap()).unwrap() + } else { + PathBuf::from(fixed_url.clone()) + } + } + }; + // fix extension & add hash placeholder + match resolved_path.extension() { + Some(os_str) => match os_str.to_str() { + Some(s) => match s { + "js" | "jsx" | "ts" | "tsx" | "mjs" => { + let mut filename = resolved_path + .file_name() + .unwrap() + .to_str() + .unwrap() + .trim_end_matches(s) + .to_owned(); + if !is_remote && !self.specifier_is_remote { + filename.push_str(HASH_PLACEHOLDER.as_str()); + filename.push('.'); + } + filename.push_str("js"); + resolved_path.set_file_name(filename); + } + _ => { + if !is_remote && !self.specifier_is_remote { + let mut filename = resolved_path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned(); + filename.push('.'); + filename.push_str(HASH_PLACEHOLDER.as_str()); + filename.push_str(".js"); + resolved_path.set_file_name(filename); + } + } + }, + None => {} + }, + None => {} + }; + self.dep_graph.push(DependencyDescriptor { + specifier: fixed_url.clone(), + is_dynamic, + }); + let path = resolved_path.to_slash().unwrap(); + if !path.starts_with("./") && !path.starts_with("../") && !path.starts_with("/") { + return (format!("./{}", path), fixed_url); + } + (path, fixed_url) + } +} + +pub fn aleph_resolve_fold(resolver: Rc>) -> impl Fold { + AlephResolveFold { + deno_hooks_idx: 0, + resolver, + } +} + +pub struct AlephResolveFold { + deno_hooks_idx: i32, + resolver: Rc>, +} + +impl AlephResolveFold { + fn new_use_deno_hook_ident(&mut self) -> String { + let resolver = self.resolver.borrow_mut(); + self.deno_hooks_idx = self.deno_hooks_idx + 1; + let path = format!("{}-{}", resolver.specifier, self.deno_hooks_idx); + let mut ident: String = "useDeno-".to_owned(); + let mut hasher = Sha1::new(); + hasher.update(path.as_bytes()); + let hash = hasher.finalize(); + ident.push_str(format!("{:x}", hash).as_str()); + ident + } +} + +impl Fold for AlephResolveFold { + noop_fold_type!(); + + // resolve import/export url + // [/pages/index.tsx] + // - development mode: + // - `import React, {useState} from "https://esm.sh/react"` -> `import React, {useState} from "/-/esm.sh/react.js"` + // - `import * as React from "https://esm.sh/react"` -> `import * as React from "/-/esm.sh/react.js"` + // - `import Logo from "../components/logo.tsx"` -> `import Logo from "/components/logo.{HASH_PLACEHOLDER}.js"` + // - `import Logo from "@/components/logo.tsx"` -> `import Logo from "/components/logo.{HASH_PLACEHOLDER}.js"` + // - `import "../style/index.css" -> `import "/style/index.css.{HASH_PLACEHOLDER}.js"` + // - `export React, {useState} from "https://esm.sh/react"` -> `export React, {useState} from * from "/-/esm.sh/react.js"` + // - `export * from "https://esm.sh/react"` -> `export * from "/-/esm.sh/react.js"` + // - bundling mode: + // - `import React, {useState} from "https://esm.sh/react"` -> `var React = __ALEPH.pack["https://esm.sh/react"].default, useState = __ALEPH__.PACK["https://esm.sh/react"].useState;` + // - `import * as React from "https://esm.sh/react"` -> `var React = __ALEPH.pack["https://esm.sh/react"]` + // - `import Logo from "../components/logo.tsx"` -> `var Logo = __ALEPH.pack["/components/logo.tsx"].default` + // - `import Logo from "@/components/logo.tsx"` -> `var Logo = __ALEPH.pack["/components/logo.tsx"].default` + // - `export React, {useState} from "https://esm.sh/react"` -> `__ALEPH.exportFrom("/pages/index.tsx", "https://esm.sh/react", {"default": "React", "useState": "useState'})` + // - `export * as React from "https://esm.sh/react"` -> `__ALEPH.exportFrom("/pages/index.tsx", "https://esm.sh/react", {"*": "React"})` + // - `export * from "https://esm.sh/react"` -> `__ALEPH.exportFrom("/pages/index.tsx", "https://esm.sh/react", "*")` + // - remove `import "../shared/iife.ts"` (push to dep_graph) + fn fold_module_items(&mut self, module_items: Vec) -> Vec { + let mut items = Vec::::new(); + + for item in module_items { + match item { + ModuleItem::ModuleDecl(decl) => { + let item: ModuleItem = match decl { + ModuleDecl::Import(import_decl) => { + if import_decl.type_only { + ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) + } else { + let mut resolver = self.resolver.borrow_mut(); + let (resolved_path, fixed_url) = + resolver.resolve(import_decl.src.value.as_ref(), false); + if resolver.bundle_mode + && (is_remote_url(fixed_url.as_str()) + || resolver.bundled_paths.contains(fixed_url.as_str())) + { + let mut var_decls: Vec = vec![]; + import_decl + .specifiers + .into_iter() + .for_each(|specifier| match specifier { + // import { default as React, useState } from "https://esm.sh/react" + ImportSpecifier::Named(ImportNamedSpecifier { + local, imported, .. + }) => { + var_decls.push(create_aleph_pack_var_decl( + local.clone(), + fixed_url.as_str(), + Some( + match imported { + Some(name) => name, + None => local, + } + .sym + .as_ref(), + ), + )); + } + // import React from "https://esm.sh/react" + ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) => { + var_decls.push(create_aleph_pack_var_decl( + local, + fixed_url.as_str(), + Some("default"), + )); + } + // import * as React from "https://esm.sh/react" + ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + var_decls.push(create_aleph_pack_var_decl(local, fixed_url.as_str(), None)); + } + }); + if var_decls.len() > 0 { + // var React = __ALEPH.pack["https://esm.sh/react"].default, useState = __ALEPH.pack["https://esm.sh/react"].useState; + ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: false, + decls: var_decls, + }))) + } else { + ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + } else { + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + src: new_js_str(resolved_path), + ..import_decl + })) + } + } + } + // export { default as React, useState } from "https://esm.sh/react" + ModuleDecl::ExportNamed(NamedExport { + type_only, + specifiers, + src: Some(src), + .. + }) => { + if type_only { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers, + src: Some(src), + type_only: true, + })) + } else { + let mut resolver = self.resolver.borrow_mut(); + let (resolved_path, fixed_url) = resolver.resolve(src.value.as_ref(), false); + if resolver.bundle_mode + && (is_remote_url(fixed_url.as_str()) + || resolver.bundled_paths.contains(fixed_url.as_str())) + { + // __ALEPH.exportFrom("/pages/index.tsx", "https://esm.sh/react", {"default": "React", "useState": "useState'}) + let call = CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("exportFrom")))), + args: vec![ + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(resolver.specifier.clone())))), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(fixed_url)))), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: specifiers + .clone() + .into_iter() + .map(|specifier| match specifier { + // export Foo from ".." + ExportSpecifier::Default(ExportDefaultSpecifier { exported }) => { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Str(new_js_str("default".into())), + value: Box::new(Expr::Lit(Lit::Str(new_js_str( + exported.sym.as_ref().into(), + )))), + }))) + } + // export {Foo, bar: Bar} from ".." + ExportSpecifier::Named(ExportNamedSpecifier { + orig, + exported, + .. + }) => PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Str(new_js_str(orig.as_ref().into())), + value: Box::new(Expr::Lit(Lit::Str(new_js_str( + (match exported { + Some(name) => name, + None => orig, + }) + .as_ref() + .into(), + )))), + }))), + // export * as Foo from ".." + ExportSpecifier::Namespace(ExportNamespaceSpecifier { + name, .. + }) => PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Str(new_js_str("*".into())), + value: Box::new(Expr::Lit(Lit::Str(new_js_str( + name.sym.as_ref().into(), + )))), + }))), + }) + .collect::>(), + })), + }, + ], + type_args: None, + }; + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("__ALEPH")))), + prop: Box::new(Expr::Call(call)), + computed: false, + })), + })) + } else { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers, + src: Some(new_js_str(resolved_path)), + type_only: false, + })) + } + } + } + // export * from "https://esm.sh/react" + ModuleDecl::ExportAll(ExportAll { src, .. }) => { + let mut resolver = self.resolver.borrow_mut(); + let (resolved_path, fixed_url) = resolver.resolve(src.value.as_ref(), false); + if resolver.bundle_mode + && (is_remote_url(fixed_url.as_str()) + || resolver.bundled_paths.contains(fixed_url.as_str())) + { + // __ALEPH.exportFrom("/pages/index.tsx", "https://esm.sh/react", "*") + let call = CallExpr { + span: DUMMY_SP, + callee: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("exportFrom")))), + args: vec![ + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(resolver.specifier.clone())))), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(fixed_url)))), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str("*".into())))), + }, + ], + type_args: None, + }; + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("__ALEPH")))), + prop: Box::new(Expr::Call(call)), + computed: false, + })), + })) + } else { + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll { + span: DUMMY_SP, + src: new_js_str(resolved_path.into()), + })) + } + } + _ => ModuleItem::ModuleDecl(decl), + }; + items.push(item.fold_children_with(self)); + } + _ => { + items.push(item.fold_children_with(self)); + } + }; + } + + items + } + + // resolve dynamic import url & sign useDeno hook + // - `import("https://esm.sh/rect")` -> `import("/-/esm.sh/react.js")` + // - `useDeno(() => {})` -> `useDeno(() => {}, false, "useDeno.RANDOM_KEY")` + fn fold_call_expr(&mut self, mut call: CallExpr) -> CallExpr { + if is_call_expr_by_name(&call, "import") { + let url = match call.args.first() { + Some(ExprOrSpread { expr, .. }) => match expr.as_ref() { + Expr::Lit(lit) => match lit { + Lit::Str(s) => s.value.as_ref(), + _ => return call, + }, + _ => return call, + }, + _ => return call, + }; + let mut resolver = self.resolver.borrow_mut(); + call.args = vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str( + resolver.resolve(url, true).0, + )))), + }]; + } else if is_call_expr_by_name(&call, "useDeno") { + let has_callback = match call.args.first() { + Some(ExprOrSpread { expr, .. }) => match expr.as_ref() { + Expr::Fn(_) => true, + Expr::Arrow(_) => true, + _ => false, + }, + _ => false, + }; + if has_callback { + let id = self.new_use_deno_hook_ident(); + if call.args.len() == 1 { + call.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: false, + }))), + }); + } + if call.args.len() > 2 { + call.args[2] = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(id.clone())))), + }; + } else { + call.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(new_js_str(id.clone())))), + }); + } + let mut resolver = self.resolver.borrow_mut(); + resolver.dep_graph.push(DependencyDescriptor { + specifier: "#".to_owned() + id.clone().as_str(), + is_dynamic: false, + }); + } + } + call + } +} + +pub fn is_remote_url(url: &str) -> bool { + return url.starts_with("https://") || url.starts_with("http://"); +} + +pub fn is_call_expr_by_name(call: &CallExpr, name: &str) -> bool { + let callee = match &call.callee { + ExprOrSuper::Super(_) => return false, + ExprOrSuper::Expr(callee) => callee.as_ref(), + }; + + match callee { + Expr::Ident(id) => id.sym.as_ref().eq(name), + _ => false, + } +} + +pub fn create_aleph_pack_var_decl(ident: Ident, url: &str, prop: Option<&str>) -> VarDeclarator { + let m = Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("__ALEPH")))), + prop: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(quote_ident!("pack")))), + prop: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: url.into(), + has_escape: false, + kind: Default::default(), + }))), + computed: true, + })), + computed: false, + }); + + match prop { + Some(prop) => VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(ident), + init: Some(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: ExprOrSuper::Expr(Box::new(m)), + prop: Box::new(Expr::Ident(quote_ident!(prop))), + computed: false, + }))), + definite: false, + }, + None => VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(ident), + init: Some(Box::new(m)), + definite: false, + }, + } +} + +fn new_js_str(str: String) -> Str { + Str { + span: DUMMY_SP, + value: str.into(), + has_escape: false, + kind: Default::default(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::import_map::ImportHashMap; + use std::collections::HashMap; + + #[test] + fn test_resolver_fix_import_url() { + let resolver = Resolver::new("/app.tsx", ImportHashMap::default(), None, false, vec![]); + assert_eq!( + resolver.fix_import_url("https://esm.sh/react"), + "/-/esm.sh/react.js" + ); + assert_eq!( + resolver.fix_import_url("https://esm.sh/react@17.0.1?target=es2015&dev"), + "/-/esm.sh/react@17.0.1_target=es2015&dev.js" + ); + assert_eq!( + resolver.fix_import_url("http://localhost:8080/mod"), + "/-/http_localhost_8080/mod.js" + ); + assert_eq!( + resolver.fix_import_url("/components/foo/./logo.tsx"), + "/components/foo/logo.tsx" + ); + assert_eq!( + resolver.fix_import_url("/components/foo/../logo.tsx"), + "/components/logo.tsx" + ); + assert_eq!( + resolver.fix_import_url("/components/../foo/logo.tsx"), + "/foo/logo.tsx" + ); + assert_eq!( + resolver.fix_import_url("/components/logo.tsx"), + "/components/logo.tsx" + ); + assert_eq!( + resolver.fix_import_url("@/components/logo.tsx"), + "/components/logo.tsx" + ); + assert_eq!( + resolver.fix_import_url("../components/logo.tsx"), + "../components/logo.tsx" + ); + assert_eq!(resolver.fix_import_url("./button.tsx"), "./button.tsx"); + } + + #[test] + fn test_resolve_local() { + let mut imports: HashMap> = HashMap::new(); + imports.insert("react".into(), vec!["https://esm.sh/react".into()]); + imports.insert( + "react-dom/".into(), + vec!["https://esm.sh/react-dom/".into()], + ); + imports.insert( + "https://deno.land/x/aleph/".into(), + vec!["http://localhost:9006/".into()], + ); + let mut resolver = Resolver::new( + "/pages/index.tsx", + ImportHashMap { + imports, + scopes: HashMap::new(), + }, + Some("17.0.1".into()), + false, + vec![], + ); + assert_eq!( + resolver.resolve("https://esm.sh/react", false), + ( + "../-/esm.sh/react@17.0.1.js".into(), + "https://esm.sh/react@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react-refresh", false), + ( + "../-/esm.sh/react-refresh.js".into(), + "https://esm.sh/react-refresh".into() + ) + ); + assert_eq!( + resolver.resolve("https://deno.land/x/aleph/framework/react/link.ts", false), + ( + "../-/http_localhost_9006/framework/react/link.js".into(), + "http://localhost:9006/framework/react/link.ts".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react@16", false), + ( + "../-/esm.sh/react@17.0.1.js".into(), + "https://esm.sh/react@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react-dom", false), + ( + "../-/esm.sh/react-dom@17.0.1.js".into(), + "https://esm.sh/react-dom@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react-dom@16.14.0", false), + ( + "../-/esm.sh/react-dom@17.0.1.js".into(), + "https://esm.sh/react-dom@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react-dom/server", false), + ( + "../-/esm.sh/react-dom@17.0.1/server.js".into(), + "https://esm.sh/react-dom@17.0.1/server".into() + ) + ); + assert_eq!( + resolver.resolve("https://esm.sh/react-dom@16.13.1/server", false), + ( + "../-/esm.sh/react-dom@17.0.1/server.js".into(), + "https://esm.sh/react-dom@17.0.1/server".into() + ) + ); + assert_eq!( + resolver.resolve("react-dom/server", false), + ( + "../-/esm.sh/react-dom@17.0.1/server.js".into(), + "https://esm.sh/react-dom@17.0.1/server".into() + ) + ); + assert_eq!( + resolver.resolve("react", false), + ( + "../-/esm.sh/react@17.0.1.js".into(), + "https://esm.sh/react@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("https://deno.land/x/aleph/mod.ts", false), + ( + "../-/http_localhost_9006/mod.js".into(), + "http://localhost:9006/mod.ts".into() + ) + ); + assert_eq!( + resolver.resolve("../components/logo.tsx", false), + ( + format!("../components/logo.{}.js", HASH_PLACEHOLDER.as_str()), + "/components/logo.tsx".into() + ) + ); + assert_eq!( + resolver.resolve("../styles/app.css", false), + ( + format!("../styles/app.css.{}.js", HASH_PLACEHOLDER.as_str()), + "/styles/app.css".into() + ) + ); + assert_eq!( + resolver.resolve("@/components/logo.tsx", false), + ( + format!("../components/logo.{}.js", HASH_PLACEHOLDER.as_str()), + "/components/logo.tsx".into() + ) + ); + assert_eq!( + resolver.resolve("~/components/logo.tsx", false), + ( + format!("../components/logo.{}.js", HASH_PLACEHOLDER.as_str()), + "/components/logo.tsx".into() + ) + ); + } + + #[test] + fn test_resolve_remote_1() { + let mut resolver = Resolver::new( + "https://esm.sh/react-dom", + ImportHashMap::default(), + Some("17.0.1".into()), + false, + vec![], + ); + assert_eq!( + resolver.resolve("https://cdn.esm.sh/react@17.0.1/es2020/react.js", false), + ( + "../cdn.esm.sh/react@17.0.1/es2020/react.js".into(), + "https://cdn.esm.sh/react@17.0.1/es2020/react.js".into() + ) + ); + assert_eq!( + resolver.resolve("./react", false), + ( + "./react@17.0.1.js".into(), + "https://esm.sh/react@17.0.1".into() + ) + ); + assert_eq!( + resolver.resolve("/react", false), + ( + "./react@17.0.1.js".into(), + "https://esm.sh/react@17.0.1".into() + ) + ); + } + + #[test] + fn test_resolve_remote_2() { + let mut resolver = Resolver::new( + "https://esm.sh/preact/hooks", + ImportHashMap::default(), + None, + false, + vec![], + ); + assert_eq!( + resolver.resolve("https://cdn.esm.sh/preact@10.5.7/es2020/preact.js", false), + ( + "../../cdn.esm.sh/preact@10.5.7/es2020/preact.js".into(), + "https://cdn.esm.sh/preact@10.5.7/es2020/preact.js".into() + ) + ); + assert_eq!( + resolver.resolve("../preact", false), + ("../preact.js".into(), "https://esm.sh/preact".into()) + ); + assert_eq!( + resolver.resolve("/preact", false), + ("../preact.js".into(), "https://esm.sh/preact".into()) + ); + } +} diff --git a/compiler/src/source_type.rs b/compiler/src/source_type.rs new file mode 100644 index 000000000..87b95c068 --- /dev/null +++ b/compiler/src/source_type.rs @@ -0,0 +1,138 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use serde::{Serialize, Serializer}; +use std::{ + fmt, + path::{Path, PathBuf}, +}; + +#[repr(i32)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum SourceType { + JavaScript = 0, + JSX = 1, + TypeScript = 2, + TSX = 3, + Json = 4, + Wasm = 5, + Unknown = 9, +} + +impl fmt::Display for SourceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + SourceType::JavaScript => "JavaScript", + SourceType::JSX => "JSX", + SourceType::TypeScript => "TypeScript", + SourceType::TSX => "TSX", + SourceType::Json => "Json", + SourceType::Wasm => "Wasm", + SourceType::Unknown => "Unknown", + }; + write!(f, "{}", value) + } +} + +impl<'a> From<&'a Path> for SourceType { + fn from(path: &'a Path) -> Self { + SourceType::from_path(path) + } +} + +impl<'a> From<&'a PathBuf> for SourceType { + fn from(path: &'a PathBuf) -> Self { + SourceType::from_path(path) + } +} + +impl<'a> From<&'a String> for SourceType { + fn from(specifier: &'a String) -> Self { + SourceType::from_path(&PathBuf::from(specifier)) + } +} + +impl Default for SourceType { + fn default() -> Self { + SourceType::Unknown + } +} + +impl SourceType { + fn from_path(path: &Path) -> Self { + match path.extension() { + None => SourceType::Unknown, + Some(os_str) => match os_str.to_str() { + Some("ts") => SourceType::TypeScript, + Some("tsx") => SourceType::TSX, + Some("js") => SourceType::JavaScript, + Some("jsx") => SourceType::JSX, + Some("mjs") => SourceType::JavaScript, + Some("json") => SourceType::Json, + Some("wasm") => SourceType::Wasm, + _ => SourceType::Unknown, + }, + } + } +} + +impl Serialize for SourceType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = match self { + SourceType::JavaScript => 0, + SourceType::JSX => 1, + SourceType::TypeScript => 2, + SourceType::TSX => 3, + SourceType::Json => 4, + SourceType::Wasm => 5, + SourceType::Unknown => 9, + } as i32; + Serialize::serialize(&value, serializer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_map_file_extension() { + assert_eq!( + SourceType::from(Path::new("foo/bar.ts")), + SourceType::TypeScript + ); + assert_eq!(SourceType::from(Path::new("foo/bar.tsx")), SourceType::TSX); + assert_eq!( + SourceType::from(Path::new("foo/bar.js")), + SourceType::JavaScript + ); + assert_eq!(SourceType::from(Path::new("foo/bar.jsx")), SourceType::JSX); + assert_eq!( + SourceType::from(Path::new("foo/bar.json")), + SourceType::Json + ); + assert_eq!( + SourceType::from(Path::new("foo/bar.wasm")), + SourceType::Wasm + ); + assert_eq!( + SourceType::from(Path::new("foo/bar.txt")), + SourceType::Unknown + ); + assert_eq!(SourceType::from(Path::new("foo/bar")), SourceType::Unknown); + } + + #[test] + fn test_display() { + assert_eq!(format!("{}", SourceType::JavaScript), "JavaScript"); + assert_eq!(format!("{}", SourceType::JSX), "JSX"); + assert_eq!(format!("{}", SourceType::TypeScript), "TypeScript"); + assert_eq!(format!("{}", SourceType::TSX), "TSX"); + assert_eq!(format!("{}", SourceType::Json), "Json"); + assert_eq!(format!("{}", SourceType::Wasm), "Wasm"); + assert_eq!(format!("{}", SourceType::Unknown), "Unknown"); + } +} diff --git a/compiler/src/swc.rs b/compiler/src/swc.rs new file mode 100644 index 000000000..1c8837b7d --- /dev/null +++ b/compiler/src/swc.rs @@ -0,0 +1,609 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// Copyright 2020-2021 postUI Lab. All rights reserved. MIT license. + +use crate::compat_fixer::compat_fixer_fold; +use crate::error::{DiagnosticBuffer, ErrorBuffer}; +use crate::fast_refresh::fast_refresh_fold; +use crate::jsx::aleph_jsx_fold; +use crate::resolve::{aleph_resolve_fold, Resolver}; +use crate::source_type::SourceType; + +use std::{cell::RefCell, path::Path, rc::Rc}; +use swc_common::{ + chain, + comments::SingleThreadedComments, + errors::{Handler, HandlerFlags}, + FileName, Globals, Mark, SourceMap, +}; +use swc_ecma_transforms_compat::{es2015, es2016, es2017, es2018, es2020}; +use swc_ecma_transforms_proposal::decorators; +use swc_ecma_transforms_typescript::strip; +use swc_ecmascript::{ + ast::{Module, Program}, + codegen::{text_writer::JsWriter, Node}, + parser::{lexer::Lexer, EsConfig, JscTarget, StringInput, Syntax, TsConfig}, + transforms::{fixer, helpers, hygiene, pass::Optional, react}, + visit::{Fold, FoldWith}, +}; + +/// Options for transpiling a module. +#[derive(Debug, Clone)] +pub struct EmitOptions { + pub target: JscTarget, + pub jsx_factory: String, + pub jsx_fragment_factory: String, + pub is_dev: bool, + pub source_map: bool, +} + +impl Default for EmitOptions { + fn default() -> Self { + EmitOptions { + target: JscTarget::Es2020, + jsx_factory: "React.createElement".into(), + jsx_fragment_factory: "React.Fragment".into(), + is_dev: false, + source_map: false, + } + } +} + +#[derive(Clone)] +pub struct ParsedModule { + pub specifier: String, + pub module: Module, + pub source_type: SourceType, + pub source_map: Rc, + pub comments: SingleThreadedComments, +} + +impl ParsedModule { + /// parse the source of the module. + /// + /// ### Arguments + /// + /// - `specifier` - The module specifier for the module. + /// - `source` - The source code for the module. + /// - `target` - The target for the module. + /// + pub fn parse( + specifier: &str, + source: &str, + source_type: Option, + ) -> Result { + let source_map = SourceMap::default(); + let source_file = source_map.new_source_file( + FileName::Real(Path::new(specifier).to_path_buf()), + source.into(), + ); + let sm = &source_map; + let error_buffer = ErrorBuffer::new(); + let source_type = match source_type { + Some(source_type) => source_type, + None => SourceType::from(Path::new(specifier)), + }; + let syntax = get_syntax(&source_type); + let input = StringInput::from(&*source_file); + let comments = SingleThreadedComments::default(); + let lexer = Lexer::new(syntax, JscTarget::Es2020, input, Some(&comments)); + let mut parser = swc_ecmascript::parser::Parser::new_from(lexer); + let handler = Handler::with_emitter_and_flags( + Box::new(error_buffer.clone()), + HandlerFlags { + can_emit_warnings: true, + dont_buffer_diagnostics: true, + ..HandlerFlags::default() + }, + ); + let module = parser + .parse_module() + .map_err(move |err| { + let mut diagnostic = err.into_diagnostic(&handler); + diagnostic.emit(); + DiagnosticBuffer::from_error_buffer(error_buffer, |span| sm.lookup_char_pos(span.lo)) + }) + .unwrap(); + + Ok(ParsedModule { + specifier: specifier.into(), + module, + source_type, + source_map: Rc::new(source_map), + comments, + }) + } + + /// Transform a JS/TS/JSX file into a JS file, based on the supplied options. + /// + /// ### Arguments + /// + /// - `resolver` - a resolver to resolve import/export url. + /// - `options` - the options for emit code. + /// + pub fn transpile( + self, + resolver: Rc>, + options: &EmitOptions, + ) -> Result<(String, Option), anyhow::Error> { + swc_common::GLOBALS.set(&Globals::new(), || { + let specifier_is_remote = resolver.borrow_mut().specifier_is_remote; + let is_ts = match self.source_type { + SourceType::TypeScript => true, + SourceType::TSX => true, + _ => false, + }; + let is_jsx = match self.source_type { + SourceType::JSX => true, + SourceType::TSX => true, + _ => false, + }; + let (aleph_jsx_fold, aleph_jsx_builtin_resolve_fold) = aleph_jsx_fold( + resolver.clone(), + self.source_map.clone(), + options.is_dev && !specifier_is_remote, + ); + let root_mark = Mark::fresh(Mark::root()); + let mut passes = chain!( + aleph_resolve_fold(resolver.clone()), + Optional::new(aleph_jsx_fold, is_jsx), + Optional::new(aleph_jsx_builtin_resolve_fold, is_jsx), + Optional::new( + fast_refresh_fold( + "$RefreshReg$", + "$RefreshSig$", + false, + self.source_map.clone() + ), + options.is_dev && !specifier_is_remote + ), + Optional::new( + react::jsx( + self.source_map.clone(), + Some(&self.comments), + react::Options { + pragma: options.jsx_factory.clone(), + pragma_frag: options.jsx_fragment_factory.clone(), + // this will use `Object.assign()` instead of the `_extends` helper when spreading props. + use_builtins: true, + ..Default::default() + }, + ), + is_jsx + ), + decorators::decorators(decorators::Config { + legacy: true, + emit_metadata: false + }), + Optional::new(es2020(), options.target < JscTarget::Es2020), + Optional::new(strip(), is_ts), + Optional::new(es2018(), options.target < JscTarget::Es2018), + Optional::new(es2017(), options.target < JscTarget::Es2017), + Optional::new(es2016(), options.target < JscTarget::Es2016), + Optional::new( + es2015(root_mark, Default::default()), + options.target < JscTarget::Es2015 + ), + Optional::new(compat_fixer_fold(), options.target < JscTarget::Es2015), + Optional::new( + helpers::inject_helpers(), + options.target < JscTarget::Es2020 + ), + Optional::new(hygiene(), options.target < JscTarget::Es2020), + fixer(Some(&self.comments)), + ); + + self.apply_transform(&mut passes, options.source_map) + }) + } + + /// Apply transform with fold. + pub fn apply_transform( + &self, + mut tr: T, + source_map: bool, + ) -> Result<(String, Option), anyhow::Error> { + let program = Program::Module(self.module.clone()); + let program = + helpers::HELPERS.set(&helpers::Helpers::new(false), || program.fold_with(&mut tr)); + let mut buf = Vec::new(); + let mut src_map_buf = Vec::new(); + let src_map = if source_map { + Some(&mut src_map_buf) + } else { + None + }; + { + let writer = Box::new(JsWriter::new( + self.source_map.clone(), + "\n", + &mut buf, + src_map, + )); + let mut emitter = swc_ecmascript::codegen::Emitter { + cfg: swc_ecmascript::codegen::Config { + minify: false, // todo: use swc minify in the future, currently use terser + }, + comments: Some(&self.comments), + cm: self.source_map.clone(), + wr: writer, + }; + program.emit_with(&mut emitter).unwrap(); + } + let src = String::from_utf8(buf).unwrap(); + if source_map { + let mut buf = Vec::new(); + self + .source_map + .build_source_map_from(&mut src_map_buf, None) + .to_writer(&mut buf) + .unwrap(); + Ok((src, Some(String::from_utf8(buf).unwrap()))) + } else { + Ok((src, None)) + } + } +} + +fn get_es_config(jsx: bool) -> EsConfig { + EsConfig { + class_private_methods: true, + class_private_props: true, + class_props: true, + dynamic_import: true, + export_default_from: true, + export_namespace_from: true, + import_meta: true, + jsx, + nullish_coalescing: true, + num_sep: true, + optional_chaining: true, + top_level_await: true, + ..EsConfig::default() + } +} + +fn get_ts_config(tsx: bool) -> TsConfig { + TsConfig { + tsx, + decorators: true, + dynamic_import: true, + ..TsConfig::default() + } +} + +fn get_syntax(source_type: &SourceType) -> Syntax { + match source_type { + SourceType::JavaScript => Syntax::Es(get_es_config(false)), + SourceType::JSX => Syntax::Es(get_es_config(true)), + SourceType::TypeScript => Syntax::Typescript(get_ts_config(false)), + SourceType::TSX => Syntax::Typescript(get_ts_config(true)), + _ => Syntax::Es(get_es_config(false)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aleph::VERSION; + use crate::import_map::ImportHashMap; + use crate::resolve::{DependencyDescriptor, Resolver, HASH_PLACEHOLDER}; + + fn t(specifer: &str, source: &str, bundling: bool) -> (String, Rc>) { + let module = ParsedModule::parse(specifer, source, None).expect("could not parse module"); + let resolver = Rc::new(RefCell::new(Resolver::new( + specifer, + ImportHashMap::default(), + None, + bundling, + vec![], + ))); + let (code, _) = module + .transpile(resolver.clone(), &EmitOptions::default()) + .expect("could not transpile module"); + (code, resolver) + } + + #[test] + fn test_transpile_ts() { + let source = r#" + enum D { + A, + B, + C, + } + + function enumerable(value: boolean) { + return function ( + _target: any, + _propertyKey: string, + descriptor: PropertyDescriptor, + ) { + descriptor.enumerable = value; + }; + } + + export class A { + private b: string; + protected c: number = 1; + e: "foo"; + constructor (public d = D.A) { + const e = "foo" as const; + this.e = e; + } + @enumerable(false) + bar() {} + } + "#; + let (code, _) = t("https://deno.land/x/mod.ts", source, false); + println!("{}", code); + assert!(code.contains("var D;\n(function(D) {\n")); + assert!(code.contains("_applyDecoratedDescriptor(")); + } + + #[test] + fn test_transpile_jsx() { + let source = r#" + import React from "https://esm.sh/react" + export default function Index() { + return ( + <> +

Hello World

+ + ) + } + "#; + let (code, _) = t("/pages/index.tsx", source, false); + println!("{}", code); + assert!(code.contains("React.createElement(React.Fragment, null")); + assert!(code.contains("React.createElement(\"h1\", {")); + assert!(code.contains("className: \"title\"")); + assert!(code.contains("import React from \"../-/esm.sh/react.js\"")); + } + + #[test] + fn test_transpile_use_deno() { + let source = r#" + export default function Index() { + const verison = useDeno(() => Deno.version) + const V8 = () => { + const verison = useDeno(() => Deno.version, true) + return

v8 v{version.v8}

+ } + const TS = () => { + const verison = useDeno(() => Deno.version, 1) + return

typescript v{version.typescript}

+ } + return ( + <> +

Deno v{version.deno}

+ + + + ) + } + "#; + let (code, _) = t("/pages/index.tsx", source, false); + println!("{}", code); + assert!(code.contains(", false, \"useDeno-")); + assert!(code.contains(", true, \"useDeno-")); + assert!(code.contains(", 1, \"useDeno-")); + } + + #[test] + fn test_transpile_jsx_builtin_tags() { + let source = r#" + import React from "https://esm.sh/react" + export default function Index() { + return ( + <> + About + About + About + + + + + + + ) + } + "#; + let (code, resolver) = t("/pages/index.tsx", source, false); + println!("{}", code); + assert!(code.contains( + format!( + "import __ALEPH_Anchor from \"../-/deno.land/x/aleph@v{}/framework/react/anchor.js\"", + VERSION.as_str() + ) + .as_str() + )); + assert!(code.contains( + format!( + "import __ALEPH_Head from \"../-/deno.land/x/aleph@v{}/framework/react/head.js\"", + VERSION.as_str() + ) + .as_str() + )); + assert!(code.contains( + format!( + "import __ALEPH_Link from \"../-/deno.land/x/aleph@v{}/framework/react/link.js\"", + VERSION.as_str() + ) + .as_str() + )); + assert!(code.contains( + format!( + "import __ALEPH_Script from \"../-/deno.land/x/aleph@v{}/framework/react/script.js\"", + VERSION.as_str() + ) + .as_str() + )); + assert!(code.contains("React.createElement(\"a\",")); + assert!(code.contains("React.createElement(__ALEPH_Anchor,")); + assert!(code.contains("React.createElement(__ALEPH_Head,")); + assert!(code.contains("React.createElement(__ALEPH_Link,")); + assert!(code.contains( + format!( + "href: \"../style/index.css.{}.js\"", + HASH_PLACEHOLDER.as_str() + ) + .as_str() + )); + assert!(code.contains("__url: \"/style/index.css\"")); + assert!(code.contains("__base: \"/pages\"")); + assert!(code.contains("React.createElement(__ALEPH_Script,")); + let r = resolver.borrow_mut(); + assert_eq!( + r.dep_graph, + vec![ + DependencyDescriptor { + specifier: "https://esm.sh/react".into(), + is_dynamic: false, + }, + DependencyDescriptor { + specifier: "/style/index.css".into(), + is_dynamic: true, + }, + DependencyDescriptor { + specifier: format!( + "https://deno.land/x/aleph@v{}/framework/react/anchor.ts", + VERSION.as_str() + ), + is_dynamic: false, + }, + DependencyDescriptor { + specifier: format!( + "https://deno.land/x/aleph@v{}/framework/react/head.ts", + VERSION.as_str() + ), + is_dynamic: false, + }, + DependencyDescriptor { + specifier: format!( + "https://deno.land/x/aleph@v{}/framework/react/link.ts", + VERSION.as_str() + ), + is_dynamic: false, + }, + DependencyDescriptor { + specifier: format!( + "https://deno.land/x/aleph@v{}/framework/react/script.ts", + VERSION.as_str() + ), + is_dynamic: false, + } + ] + ); + } + + #[test] + fn test_transpile_inlie_style() { + let source = r#" + export default function Index() { + const [color, setColor] = useState('white'); + + return ( + <> + + + + ) + } + "#; + let (code, resolver) = t("/pages/index.tsx", source, false); + assert!(code.contains( + format!( + "import __ALEPH_Style from \"../-/deno.land/x/aleph@v{}/framework/react/style.js\"", + VERSION.as_str() + ) + .as_str() + )); + assert!(code.contains("React.createElement(__ALEPH_Style,")); + assert!(code.contains("__styleId: \"inline-style-")); + let r = resolver.borrow_mut(); + assert!(r.inline_styles.len() == 2); + } + + #[test] + fn test_transpile_bundling_import() { + let source = r#" + import React, { useState, useEffect as useEffect_ } from "https://esm.sh/react" + import * as React_ from "https://esm.sh/react" + import Logo from '../components/logo.ts' + import Nav from '../components/nav.ts' + import '../shared/iife.ts' + export default function Index() { + return ( + <> + + +