├── .gitignore ├── .assets └── screenshot.png ├── src ├── utils │ ├── get-watcher.ts │ ├── async-loop.ts │ └── environment.ts ├── backend │ ├── reporter.ts │ ├── ngc │ │ ├── host.ts │ │ ├── route.ts │ │ ├── index.ts │ │ ├── refactor.ts │ │ ├── build-loop.ts │ │ └── entry-resolver.ts │ ├── transformers │ │ ├── ast-helpers.ts │ │ ├── interfaces.ts │ │ ├── remove-decorators.ts │ │ ├── replace-bootstrap.ts │ │ ├── insert-import.ts │ │ ├── elide-imports.ts │ │ ├── make-transform.ts │ │ └── resources.ts │ ├── worker │ │ ├── client.ts │ │ ├── launcher.ts │ │ └── index.ts │ └── format.ts ├── frontend │ ├── assets │ │ ├── template.ts │ │ ├── aot.ts │ │ ├── virtual.ts │ │ └── jit.ts │ ├── utils │ │ └── collect-dependencies.ts │ ├── loaders │ │ └── template.ts │ └── patch.ts ├── interfaces.ts ├── index.ts └── modules.d.ts ├── .editorconfig ├── tsconfig.json ├── tslint.json ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | build/ 4 | node_modules/ 5 | 6 | yarn-error.log 7 | 8 | -------------------------------------------------------------------------------- /.assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fathyb/parcel-plugin-angular/HEAD/.assets/screenshot.png -------------------------------------------------------------------------------- /src/utils/get-watcher.ts: -------------------------------------------------------------------------------- 1 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 2 | 3 | // TODO: find a better way to do this 4 | export async function getWatcher(bundler: any): Promise { 5 | if(!bundler.options.watch) { 6 | return null 7 | } 8 | 9 | while(!bundler.watcher) { 10 | await sleep(5) 11 | } 12 | 13 | return bundler.watcher 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 4 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /src/backend/reporter.ts: -------------------------------------------------------------------------------- 1 | import {Diagnostic as AngularDiagnostic} from '@angular/compiler-cli/src/transformers/api' 2 | import {Diagnostic as TypeScriptDiagnostic} from 'typescript' 3 | 4 | import {formatDiagnostics} from './format' 5 | 6 | export type Diagnostic = AngularDiagnostic | TypeScriptDiagnostic 7 | 8 | export function reportDiagnostics(diagnostics: Diagnostic[]): void { 9 | if(diagnostics.length > 0 ) { 10 | const frame = formatDiagnostics(diagnostics, process.cwd()) 11 | 12 | if(frame.trim().length > 0) { 13 | console.error(frame) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | 6 | "types": ["node", "typescript"], 7 | 8 | "sourceMap": true, 9 | "declaration": true, 10 | 11 | "experimentalDecorators": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "importHelpers": true, 17 | "allowSyntheticDefaultImports": false, 18 | "moduleResolution": "node", 19 | 20 | "rootDir": "src", 21 | "outDir": "build" 22 | }, 23 | "include": [ 24 | "src/**/*.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/frontend/assets/template.ts: -------------------------------------------------------------------------------- 1 | import HTMLAssetLib = require('parcel-bundler/lib/assets/HTMLAsset') 2 | import HTMLAssetSrc = require('parcel-bundler/src/assets/HTMLAsset') 3 | 4 | import parse = require('posthtml-parser') 5 | import api = require('posthtml/lib/api') 6 | 7 | export const HTMLAsset = parseInt(process.versions.node, 10) < 8 ? HTMLAssetLib : HTMLAssetSrc 8 | 9 | /// Same as HTMLAsset but uses case-sensitive attribute names 10 | export class TemplateAsset extends HTMLAsset { 11 | public parse(code: string) { 12 | const res = parse(code) 13 | 14 | res.walk = api.walk 15 | res.match = api.match 16 | 17 | return res 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/backend/ngc/host.ts: -------------------------------------------------------------------------------- 1 | import {CompilerHost, CompilerOptions} from '@angular/compiler-cli/src/transformers/api' 2 | 3 | import {CompilerHost as LocalCompilerHost} from 'parcel-plugin-typescript/exports' 4 | 5 | export class AngularCompilerHost extends LocalCompilerHost implements CompilerHost { 6 | public readonly resources: {[path: string]: string} = {} 7 | 8 | constructor( 9 | options: CompilerOptions, 10 | private readonly compileResource: (file: string) => Promise 11 | ) { 12 | super(options) 13 | } 14 | 15 | public readResource(path: string) { 16 | return this.compileResource(path).catch(err => { 17 | // TODO: handle this 18 | console.log('\n\n') 19 | console.log('Compile error', err) 20 | console.log('\n\n') 21 | 22 | return this.readFile(path)! 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-eslint-rules"], 3 | "rules": { 4 | "no-duplicate-variable": true, 5 | "no-string-literal": false, 6 | "no-console": [false], 7 | "no-unused-variable": [true], 8 | "whitespace": [ 9 | true, 10 | "check-operator", "check-separator", "check-type", "check-typecast" 11 | ], 12 | "trailing-comma": [false], 13 | "object-literal-sort-keys": false, 14 | "interface-name": [true, "never-prefix"], 15 | "one-line": [true, "check-open-brace", "check-whitespace"], 16 | "semicolon": [true, "never"], 17 | "quotemark": [true, "single", "avoid-escape"], 18 | "indent": [true, "tabs"], 19 | "no-namespace": false, 20 | "arrow-parens": [true, "ban-single-arg-parens"], 21 | "only-arrow-functions": false, 22 | "max-classes-per-file": false, 23 | "no-bitwise": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/backend/ngc/route.ts: -------------------------------------------------------------------------------- 1 | import {dirname, relative} from 'path' 2 | 3 | import {Program} from '@angular/compiler-cli/src/transformers/api' 4 | 5 | function parseNgModule(path: string): string { 6 | return `${path.replace(/#[^]*$/, '').replace(/\.ts$/, '')}.ngfactory` 7 | } 8 | 9 | export function generateRouteLoader(main: string, program: Program) { 10 | const routes = program.listLazyRoutes() 11 | const mainDir = dirname(main) 12 | 13 | return ` 14 | var __old_systemjs__ = typeof System !== 'undefined' && System 15 | 16 | window.System = { 17 | import(module) { 18 | ${routes.map(route => 19 | `if(module === '${parseNgModule(route.route)}') { 20 | return import('${relative(mainDir, parseNgModule(route.referencedModule.filePath))}') 21 | }` 22 | ).join('\n')} 23 | 24 | if(__old_systemjs__) { 25 | return __old_systemjs__.import(module) 26 | } 27 | 28 | throw new Error('Cannot find module "' + module + '"') 29 | } 30 | } 31 | ` 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/transformers/ast-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | // Find all nodes from the AST in the subtree of node of SyntaxKind kind. 4 | export function collectDeepNodes(node: ts.Node, kind: ts.SyntaxKind): T[] { 5 | const nodes: T[] = [] 6 | const helper = (child: ts.Node) => { 7 | if(child.kind === kind) { 8 | nodes.push(child as T) 9 | } 10 | 11 | ts.forEachChild(child, helper) 12 | } 13 | 14 | ts.forEachChild(node, helper) 15 | 16 | return nodes 17 | } 18 | 19 | export function getFirstNode(sourceFile: ts.SourceFile): ts.Node | null { 20 | if(sourceFile.statements.length > 0) { 21 | return sourceFile.statements[0] || null 22 | } 23 | 24 | return null 25 | } 26 | 27 | export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null { 28 | if(sourceFile.statements.length > 0) { 29 | return sourceFile.statements[sourceFile.statements.length - 1] || null 30 | } 31 | 32 | return null 33 | } 34 | -------------------------------------------------------------------------------- /src/frontend/utils/collect-dependencies.ts: -------------------------------------------------------------------------------- 1 | import {dirname, relative} from 'path' 2 | 3 | import JSAsset = require('parcel-bundler/src/assets/JSAsset') 4 | 5 | import {Resources} from '../../interfaces' 6 | 7 | export function collectDependencies(asset: JSAsset, resources: Resources|null) { 8 | if(asset.options.__minifyUsingClosure) { 9 | // Keep ES6 imports/exports to improve Closure's tree-shaking 10 | asset.isES6Module = false 11 | 12 | // Disable Uglify, for performances and to keep types annotations 13 | asset.options.minify = false 14 | } 15 | 16 | if(!resources) { 17 | return 18 | } 19 | 20 | const dir = dirname(asset.name) 21 | 22 | resources.external.forEach(resource => { 23 | let path = relative(dir, resource) 24 | 25 | if(!/^\.\//.test(path)) { 26 | path = `./${path}` 27 | } 28 | 29 | asset.addDependency(path, {}) 30 | }) 31 | resources.bundled.forEach(resource => 32 | asset.addDependency(resource, {includedInParent: true}) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/async-loop.ts: -------------------------------------------------------------------------------- 1 | export class AsyncLoop { 2 | private running = false 3 | private readonly queue: T[] = [] 4 | private readonly watchers: Array<() => void> = [] 5 | 6 | constructor( 7 | private readonly work: (data: T[]) => Promise 8 | ) {} 9 | 10 | public async emit(data: T): Promise { 11 | this.queue.push(data) 12 | 13 | if(this.running) { 14 | return this.wait() 15 | } 16 | 17 | this.running = true 18 | 19 | try { 20 | await this.work(this.queue.splice(0)) 21 | } 22 | finally { 23 | this.running = false 24 | 25 | while(this.watchers.length > 0) { 26 | this.watchers.pop()!() 27 | } 28 | } 29 | } 30 | 31 | public wait(): Promise { 32 | if(!this.running) { 33 | return Promise.resolve() 34 | } 35 | 36 | return this.waitCurrentOrNext() 37 | } 38 | 39 | public waitCurrentOrNext(): Promise { 40 | return new Promise(resolve => 41 | this.watchers.push(resolve) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Resources { 2 | external: string[] 3 | bundled: string[] 4 | } 5 | 6 | export interface CompileRequest { 7 | file: string 8 | tsConfig: string 9 | } 10 | 11 | export interface CompileResult { 12 | sources: { 13 | js: string 14 | sourceMap?: string 15 | } 16 | resources: R 17 | } 18 | 19 | export interface Request { 20 | typeCheck: CompileRequest 21 | compile: CompileRequest 22 | readVirtualFile: string 23 | invalidate: string[] 24 | } 25 | 26 | export interface Response { 27 | typeCheck: void 28 | compile: CompileResult 29 | readVirtualFile: string|null 30 | invalidate: void 31 | } 32 | 33 | export interface ServerRequest extends Request { 34 | processResource: string 35 | } 36 | 37 | export interface ServerResponse extends Response { 38 | processResource: string 39 | } 40 | 41 | export interface WorkerRequest extends Request { 42 | wait: void 43 | getFactories: void 44 | } 45 | 46 | export interface WorkerResponse extends Response { 47 | wait: void 48 | getFactories: string[] 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fathy Boundjadj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/frontend/assets/aot.ts: -------------------------------------------------------------------------------- 1 | import {Configuration, JSAsset, loadConfiguration} from 'parcel-plugin-typescript/exports' 2 | 3 | import {IPCClient} from '../../backend/worker/client' 4 | import {Resources} from '../../interfaces' 5 | 6 | import {collectDependencies} from '../utils/collect-dependencies' 7 | 8 | export = class AngularAOTTSAsset extends JSAsset { 9 | private readonly config: Promise 10 | private resources: Resources|null = null 11 | 12 | constructor(name: string, pkg: string, options: any) { 13 | super(name, pkg, options) 14 | 15 | this.config = loadConfiguration(name) 16 | } 17 | 18 | public mightHaveDependencies() { 19 | return true 20 | } 21 | 22 | public collectDependencies() { 23 | super.collectDependencies() 24 | 25 | collectDependencies(this, this.resources) 26 | } 27 | 28 | public async parse() { 29 | const {path: tsConfig} = await this.config 30 | const result = await IPCClient.compile({tsConfig, file: this.name}) 31 | 32 | this.resources = result.resources 33 | this.contents = result.sources.js 34 | 35 | // Parse result as ast format through babylon 36 | return super.parse(this.contents) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/backend/transformers/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | export enum OPERATION_KIND { 4 | Remove, 5 | Add, 6 | Replace 7 | } 8 | 9 | export type StandardTransform = (sourceFile: ts.SourceFile) => TransformOperation[] 10 | 11 | export abstract class TransformOperation { 12 | constructor( 13 | public kind: OPERATION_KIND, 14 | public sourceFile: ts.SourceFile, 15 | public target: ts.Node 16 | ) { } 17 | } 18 | 19 | export class RemoveNodeOperation extends TransformOperation { 20 | constructor(sourceFile: ts.SourceFile, target: ts.Node) { 21 | super(OPERATION_KIND.Remove, sourceFile, target) 22 | } 23 | } 24 | 25 | export class AddNodeOperation extends TransformOperation { 26 | constructor( 27 | sourceFile: ts.SourceFile, 28 | target: ts.Node, 29 | public before?: ts.Node, 30 | public after?: ts.Node 31 | ) { 32 | super(OPERATION_KIND.Add, sourceFile, target) 33 | } 34 | } 35 | 36 | export class ReplaceNodeOperation extends TransformOperation { 37 | public kind: OPERATION_KIND.Replace 38 | constructor(sourceFile: ts.SourceFile, target: ts.Node, public replacement: ts.Node) { 39 | super(OPERATION_KIND.Replace, sourceFile, target) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/environment.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'fs' 2 | 3 | import commentsJson = require('comment-json') 4 | import findUp = require('find-up') 5 | 6 | export type AngularBuildMode = 'aot'|'jit' 7 | 8 | export interface PluginConfiguration { 9 | watch: AngularBuildMode 10 | build: AngularBuildMode 11 | } 12 | 13 | const defaultConfig: PluginConfiguration = { 14 | watch: 'jit', 15 | build: 'aot' 16 | } 17 | 18 | export function getPluginConfig(): PluginConfiguration { 19 | const path = findUp.sync('tsconfig.json') 20 | 21 | if(!path) { 22 | return defaultConfig 23 | } 24 | 25 | try { 26 | const { 27 | build = defaultConfig.build, 28 | watch = defaultConfig.watch 29 | } = commentsJson.parse(readFileSync(path, {encoding: 'utf-8'})).parcelAngularOptions || {} as PluginConfiguration 30 | 31 | if(build !== 'aot' && build !== 'jit') { 32 | throw new Error('[ParcelTypeScriptPlugin] parcelTsPluginOptions.angular.build should be a "jit" or "aot"') 33 | } 34 | 35 | if(watch !== 'aot' && watch !== 'jit') { 36 | throw new Error('[ParcelTypeScriptPlugin] parcelTsPluginOptions.angular.watch should be a "jit" or "aot"') 37 | } 38 | 39 | return {build, watch} 40 | } 41 | catch(_) { 42 | return defaultConfig 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {AngularServer} from './backend/worker' 2 | import {injectAngularSupport} from './frontend/patch' 3 | import {getPluginConfig} from './utils/environment' 4 | 5 | export = (bundler: any) => { 6 | const {watch} = bundler.options 7 | const config = getPluginConfig() 8 | let tsAsset: string|null = null 9 | 10 | if((watch && config.watch === 'aot') || (!watch && config.build === 'aot')) { 11 | // Workaround for the resolving/cache issues with Angular generated files 12 | injectAngularSupport(bundler) 13 | 14 | bundler.options.__closureKeepJSAsset = true 15 | // We register .js files for the generated ngfactory/ngstyles files 16 | bundler.addAssetType('js', require.resolve('./frontend/assets/virtual')) 17 | 18 | tsAsset = require.resolve('./frontend/assets/aot') 19 | } 20 | else { 21 | tsAsset = require.resolve('./frontend/assets/jit') 22 | } 23 | 24 | // process.send is only defined on the main process 25 | if(!process.send) { 26 | const server = new AngularServer(bundler) 27 | 28 | if(!watch) { 29 | bundler.on('buildEnd', () => server.close()) 30 | } 31 | } 32 | 33 | bundler.addAssetType('ts', tsAsset) 34 | bundler.addAssetType('tsx', tsAsset) 35 | 36 | process.env['PARCEL_PLUGIN_TYPESCRIPT_DISABLE'] = 'true' 37 | } 38 | -------------------------------------------------------------------------------- /src/frontend/assets/virtual.ts: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path' 2 | 3 | import {JSAsset} from 'parcel-plugin-typescript/exports' 4 | import resolve = require('resolve') 5 | 6 | import {IPCClient} from '../../backend/worker/client' 7 | 8 | let ClosureAsset: any|null = null 9 | 10 | export = class VirtualAsset extends JSAsset { 11 | public async load(): Promise { 12 | if(/\.ng(factory|style)\.js$/.test(this.name)) { 13 | const file = await IPCClient.readVirtualFile(this.name) 14 | 15 | if(file) { 16 | return file 17 | } 18 | } 19 | 20 | return super.load() 21 | } 22 | 23 | public collectDependencies() { 24 | if(this.options.__minifyUsingClosure) { 25 | try { 26 | if(!ClosureAsset) { 27 | const assetPath = resolve.sync('parcel-plugin-closure/build/javascript/parcel/asset', { 28 | basedir: dirname(this.name) 29 | }) 30 | 31 | ClosureAsset = require(assetPath) 32 | } 33 | 34 | ClosureAsset.prototype.collectDependencies.call(this) 35 | } 36 | catch(err) { 37 | // Keep ES6 imports/exports to improve Closure's tree-shaking 38 | this.isES6Module = false 39 | 40 | // Disable Uglify, for performances and to keep types annotations 41 | this.options.minify = false 42 | } 43 | } 44 | else { 45 | super.collectDependencies() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/backend/worker/client.ts: -------------------------------------------------------------------------------- 1 | import {getSocketPath} from 'parcel-plugin-typescript/exports' 2 | import rp = require('request-promise') 3 | 4 | import {ServerRequest, ServerResponse} from '../../interfaces' 5 | 6 | async function request = Keys>( 7 | name: string, endpoint: K, data: RQ[K] 8 | ): Promise { 9 | const response: {result?: RS[K], error?: any} = await rp({ 10 | uri: `http://unix:${getSocketPath(name)}:/${endpoint}`, 11 | method: 'POST', 12 | body: {data}, 13 | json: true 14 | }) 15 | 16 | if(response.error) { 17 | throw new Error(response.error) 18 | } 19 | 20 | return response.result! 21 | } 22 | 23 | export type Keys = (keyof T) & (keyof U) 24 | export type Client = Keys> = { 25 | [P in K]: (data: RQ[P]) => Promise 26 | } 27 | 28 | function makeClient = Keys>(name: string, keys: K[]): Client { 29 | const object: Partial> = {} 30 | 31 | keys.forEach(key => object[key] = data => request(name, key, data)) 32 | 33 | return object as Client 34 | } 35 | 36 | // TODO: use type introspection 37 | export const IPCClient = makeClient('angular', [ 38 | 'compile', 'typeCheck', 'processResource', 'readVirtualFile' 39 | ]) 40 | -------------------------------------------------------------------------------- /src/frontend/loaders/template.ts: -------------------------------------------------------------------------------- 1 | import {dirname, resolve} from 'path' 2 | 3 | import {TemplateAsset} from '../assets/template' 4 | 5 | interface CacheEntry { 6 | original: string 7 | generated: string 8 | } 9 | 10 | const cache = new Map() 11 | const fileResources = new Map() 12 | 13 | export async function processResource( 14 | file: string, pkg: string, options: {}, parser: any, code?: string 15 | ): Promise { 16 | const cached = cache.get(file) 17 | 18 | if(cached && cached.original === code) { 19 | return cached.generated 20 | } 21 | 22 | const asset = /\.html/.test(file) 23 | ? new TemplateAsset(file, pkg, options) 24 | : parser.getAsset(file, pkg, options) 25 | 26 | if(code) { 27 | asset.contents = code 28 | } 29 | 30 | const processed = await asset.process() 31 | const generated = processed[asset.type] 32 | 33 | cache.set(file, { 34 | original: asset.contents!, 35 | generated 36 | }) 37 | 38 | fileResources.set(file, Array 39 | .from(asset.dependencies.keys() as string[]) 40 | .filter(key => !/^_css_loader/.test(key)) 41 | .map(k => resolve(dirname(file), k)) 42 | ) 43 | 44 | return generated 45 | } 46 | 47 | export function getFileResources(file: string): string[] { 48 | const resource = fileResources.get(file) 49 | 50 | if(!resource) { 51 | throw new Error(`Cannot find resources for file ${file}`) 52 | } 53 | 54 | return resource 55 | } 56 | -------------------------------------------------------------------------------- /src/frontend/patch.ts: -------------------------------------------------------------------------------- 1 | import {dirname, resolve} from 'path' 2 | 3 | /** 4 | * When generating files on the fly on AOT mode we need to 5 | * override Parcel cache and resolver for it to work properly 6 | */ 7 | export function injectAngularSupport(bundler: any) { 8 | const {cache, resolver} = bundler 9 | 10 | if(cache) { 11 | const Cache = cache.constructor 12 | const CacheWrite = Cache.prototype.write 13 | const CacheRead = Cache.prototype.read 14 | 15 | cache.write = function(this: any, file: string, data: any) { 16 | if(/\.ng(factory)|(style)\.js$/.test(file) || /\.ts$/.test(file)) { 17 | return Promise.resolve() 18 | } 19 | 20 | return CacheWrite.call(this, file, data) 21 | } 22 | 23 | cache.read = function(this: any, file: string) { 24 | if(/\.ng(factory)|(style)\.js$/.test(file)) { 25 | return Promise.resolve(null) 26 | } 27 | 28 | return CacheRead.call(this, file) 29 | } 30 | } 31 | 32 | const Resolver = resolver.constructor 33 | const ResolveInternal = Resolver.prototype.resolveInternal 34 | 35 | resolver.resolveInternal = function(this: any, path: string, parent: string, ...args: any[]) { 36 | if(/\.ngfactory$/.test(path) || /\.ngstyle$/.test(path)) { 37 | return resolve(dirname(parent), `${path}.js`) 38 | } 39 | if(/\.ngfactory\.js$/.test(path) || /\.ngstyle\.js$/.test(path)) { 40 | return resolve(dirname(parent), path) 41 | } 42 | 43 | return ResolveInternal.call(this, path, parent, ...args) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-angular", 3 | "version": "0.5.1-next.10", 4 | "description": "Angular support for Parcel bundler", 5 | "author": "Fathy Boundjadj ", 6 | "license": "MIT", 7 | "repository": "https://github.com/fathyb/parcel-plugin-angular.git", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "files": [ 11 | "src", 12 | "build" 13 | ], 14 | "scripts": { 15 | "build": "tsc", 16 | "lint": "tslint --project .", 17 | "precommit": "run-p build lint" 18 | }, 19 | "peerDependencies": { 20 | "@angular/compiler": "^5.1.0", 21 | "@angular/compiler-cli": "^5.1.0", 22 | "parcel-bundler": "^1.1.0", 23 | "typescript": ">=2.4.2 <2.6", 24 | "parcel-plugin-typescript": "^0.5.1" 25 | }, 26 | "dependencies": { 27 | "@babel/code-frame": "^7.0.0-beta.34", 28 | "chalk": "^2.3.0", 29 | "comment-json": "^1.1.3", 30 | "find-up": "^2.1.0", 31 | "line-column": "^1.0.2", 32 | "normalize-path": "^2.1.1", 33 | "posthtml": "^0.11.0", 34 | "posthtml-parser": "~0.3.0", 35 | "request": "^2.34", 36 | "request-promise": "^4.2.2", 37 | "resolve": "^1.5.0", 38 | "semver": "^5.4.1", 39 | "tslib": "^1.8.1" 40 | }, 41 | "devDependencies": { 42 | "@angular/compiler": "^5.1.0", 43 | "@angular/compiler-cli": "^5.1.0", 44 | "@types/comment-json": "^1.1.0", 45 | "@types/find-up": "^2.1.1", 46 | "@types/node": "^8.0.0", 47 | "@types/request-promise": "^4.1.39", 48 | "@types/resolve": "^0.0.5", 49 | "@types/semver": "^5.4.0", 50 | "husky": "^0.14.3", 51 | "npm-run-all": "^4.1.2", 52 | "parcel-bundler": "^1.2.0", 53 | "parcel-plugin-typescript": "^0.5.1", 54 | "standard-version": "^4.2.0", 55 | "tslint": "^5.8.0", 56 | "tslint-eslint-rules": "^4.1.1", 57 | "typescript": ">=2.4.2 <2.6" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/backend/ngc/index.ts: -------------------------------------------------------------------------------- 1 | import {ParsedConfiguration, readConfiguration} from '@angular/compiler-cli' 2 | 3 | import * as ts from 'typescript' 4 | 5 | import {PathTransform} from 'parcel-plugin-typescript/exports' 6 | 7 | import {CompileResult} from '../../interfaces' 8 | import {AngularCompilerHost} from './host' 9 | import {generateRouteLoader} from './route' 10 | 11 | import {BuildLoop} from './build-loop' 12 | 13 | export class AngularCompiler { 14 | public readonly host: AngularCompilerHost 15 | public readonly loop: BuildLoop 16 | 17 | private readonly config: ParsedConfiguration 18 | private readonly transformers: Array> = [] 19 | 20 | constructor(project: string, compileResource: (file: string) => Promise) { 21 | this.config = readConfiguration(project) 22 | 23 | const {options} = this.config 24 | 25 | this.host = new AngularCompilerHost(this.config.options, compileResource) 26 | this.loop = new BuildLoop(this.host, this.config, this.transformers) 27 | 28 | this.transformers.push(PathTransform(options)) 29 | } 30 | 31 | /** 32 | * Triggers a TypeScript compilation if needed and returns a compiled file 33 | * @param path a absolute path to a TypeScript source file to compile 34 | */ 35 | public async compile(path: string): Promise> { 36 | const {config, loop} = this 37 | 38 | if(loop.entryFile === null) { 39 | // We assume the file file included by the project is the entry 40 | // It is used to inject the generated SystemJS loader 41 | // TODO: use Parcel dependencies instead 42 | loop.entryFile = path 43 | } 44 | 45 | await loop.emit(undefined) 46 | 47 | const {basePath, outDir} = config.options 48 | 49 | if(basePath && outDir) { 50 | path = path.replace(basePath, outDir) 51 | } 52 | 53 | let js = this.host.store.readFile(path.replace(/\.tsx?$/, '.js'))! 54 | const program = loop.getProgram() 55 | 56 | // detect if the file is the main module 57 | // TODO: use Parcel dependencies 58 | if(program && path === this.loop.entryFile) { 59 | js = `${js}\n${generateRouteLoader(path, program)}` 60 | } 61 | 62 | return { 63 | sources: {js}, 64 | resources: this.loop.getResources(path) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'parcel-bundler/src/assets/JSAsset' { 2 | class JSAsset { 3 | public name: string 4 | public contents?: string 5 | public options?: any 6 | public package?: any 7 | public dependencies: Map 8 | public depAssets: Map 9 | public isES6Module: boolean 10 | public parentBundle: any|null 11 | 12 | constructor(name: string, pkg: string, options: any) 13 | 14 | parse(code: string): Promise 15 | load(): Promise 16 | addURLDependency(url: string, from?: string, opts?: {}): string 17 | addDependency(url: string, opts: {}): string 18 | collectDependencies(): void 19 | } 20 | 21 | export = JSAsset 22 | } 23 | declare module 'parcel-bundler/lib/assets/JSAsset' { 24 | import JSAsset = require('parcel-bundler/src/assets/JSAsset') 25 | 26 | export = JSAsset 27 | } 28 | 29 | declare module 'parcel-bundler/src/assets/HTMLAsset' { 30 | import HTMLAsset = require('parcel-bundler/lib/assets/HTMLAsset') 31 | 32 | export = HTMLAsset 33 | } 34 | declare module 'parcel-bundler/lib/assets/HTMLAsset' { 35 | class JSAsset { 36 | public name: string 37 | public contents?: string 38 | public options?: any 39 | public package?: any 40 | public parentBundle?: any 41 | public dependencies: Map 42 | public depAssets: Map 43 | 44 | constructor(name: string, pkg: string, options: any) 45 | 46 | parse(code: string): Promise 47 | process(): Promise 48 | load(): Promise 49 | } 50 | 51 | export = JSAsset 52 | } 53 | declare module 'normalize-path' 54 | declare module 'posthtml-parser' 55 | declare module 'posthtml/lib/api' 56 | declare module 'line-column' { 57 | function lineColumn(str: string, index: number): { 58 | line: number 59 | col: number 60 | } 61 | 62 | export = lineColumn 63 | } 64 | 65 | declare module '@babel/code-frame' { 66 | export interface LineAndColumn { 67 | line: number 68 | column: number 69 | } 70 | 71 | export interface Location { 72 | start: LineAndColumn 73 | end?: LineAndColumn 74 | } 75 | 76 | export type Options = Partial<{ 77 | highlightCode: boolean 78 | linesAbove: number 79 | linesBelow: number 80 | forceColor: boolean 81 | }> 82 | 83 | export function codeFrameColumns(lines: string, location: Location, options?: Options): string 84 | } 85 | -------------------------------------------------------------------------------- /src/backend/worker/launcher.ts: -------------------------------------------------------------------------------- 1 | import {FileStore, Handler, LanguageService, loadConfiguration} from 'parcel-plugin-typescript/exports' 2 | 3 | import {WorkerRequest, WorkerResponse} from '../../interfaces' 4 | import {AngularCompiler} from '../ngc' 5 | 6 | import {IPCClient} from './client' 7 | 8 | const compilers = new Map>() 9 | const services = new Map>() 10 | 11 | const store = FileStore.shared() 12 | 13 | function getCompiler(tsConfig: string): Promise { 14 | let compiler = compilers.get(tsConfig) 15 | 16 | if(!compiler) { 17 | compiler = loadConfiguration(tsConfig).then(config => 18 | new AngularCompiler(config.path, IPCClient.processResource) 19 | ) 20 | 21 | compilers.set(tsConfig, compiler) 22 | } 23 | 24 | return compiler 25 | } 26 | 27 | function getService(tsConfig: string): Promise { 28 | let service = services.get(tsConfig) 29 | 30 | if(!service) { 31 | service = loadConfiguration(tsConfig).then(config => new LanguageService(config)) 32 | 33 | services.set(tsConfig, service) 34 | } 35 | 36 | return service 37 | } 38 | 39 | const handler: Handler = { 40 | async compile({file, tsConfig}) { 41 | const compiler = await getCompiler(tsConfig) 42 | 43 | return compiler.compile(file) 44 | }, 45 | async typeCheck({file, tsConfig}) { 46 | const service = await getService(tsConfig) 47 | 48 | // TODO: support noEmitOnError 49 | await service.check(file, true) 50 | }, 51 | async wait() { 52 | await Promise.all( 53 | Array 54 | .from(compilers.values()) 55 | .map(promise => 56 | promise.then(compiler => 57 | compiler.loop.wait() 58 | ) 59 | ) 60 | ) 61 | }, 62 | async readVirtualFile(file) { 63 | try { 64 | await handler.wait(undefined) 65 | 66 | return store.readFile(file) || null 67 | } 68 | catch { 69 | return null 70 | } 71 | }, 72 | async invalidate(files: string[]) { 73 | for(const file of files) { 74 | store.invalidate(file) 75 | } 76 | }, 77 | async getFactories() { 78 | await Promise.all( 79 | Array 80 | .from(compilers.values()) 81 | .map(promise => 82 | promise.then(compiler => 83 | compiler.loop.waitCurrentOrNext() 84 | ) 85 | ) 86 | ) 87 | 88 | return store 89 | .getFiles() 90 | .filter(file => /\.ng(factory)|(style)\.js$/.test(file)) 91 | } 92 | } 93 | 94 | export = handler 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parcel-plugin-angular 2 | 3 | Complete Angular support for Parcel and TypeScript. 4 | 5 | ![screenshot](.assets/screenshot.png) 6 | 7 | ## Features 8 | 9 | - [`parcel-plugin-typescript` features](https://github.com/fathyb/parcel-plugin-typescript#features) 10 | - AOT compilation : using the official Angular compiler for smaller and faster applications. 11 | - Lazy Loading : the plugin automagically splits your Angular modules in multiple JavaScript files with Parcel when you use lazy routes. 12 | - Template and style parsing : your templates and style are processed by Parcel to find and replace resources. 13 | - Transformations (based on [`angular/angular-cli`](https://github.com/angular/angular-cli) transformers) : 14 | - It removes all your Angular decorators in AOT mode for smaller bundles 15 | - It replaces JIT bootstrap code with AOT when it's used. You can keep one main file using the `@angular/platform-browser-dynamic` module, see [Entry file](#entry-file) 16 | 17 | ## Prerequisites 18 | 19 | - `@angular/compiler` and `@angular/compiler-cli` should be installed 20 | - `parcel-plugin-typescript` should not be installed 21 | 22 | ## Installation 23 | 24 | `yarn add parcel-plugin-angular --dev` 25 | 26 | or 27 | 28 | `npm install parcel-plugin-angular --save-dev` 29 | 30 | ## Configuration 31 | 32 | You can pass a `parcelAngularOptions` object in your `tsconfig.json`, here are the defaults : 33 | ```js 34 | { 35 | "compilerOptions": { ... }, 36 | // the plugin options 37 | "parcelAngularOptions": { 38 | // What compiler should we use when watching or serving 39 | "watch": "jit", 40 | 41 | // What compiler should we use when building (parcel build) 42 | "build": "aot" 43 | } 44 | } 45 | ``` 46 | 47 | ## Entry file 48 | 49 | To make it easy to switch between JIT and AOT mode we automatically translate your JIT bootstrap code to AOT if you are using the AOT compiler. 50 | 51 | ```ts 52 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic' 53 | import {enableProdMode} from '@angular/core' 54 | import {AppModule} from './app/app.module' 55 | 56 | if(process.env.NODE_ENV === 'production') { 57 | enableProdMode() 58 | } 59 | 60 | platformBrowserDynamic().bootstrapModule(AppModule) 61 | ``` 62 | 63 | will be transformed to : 64 | 65 | ```ts 66 | import {platformBrowser} from '@angular/platform-browser' 67 | import {enableProdMode} from '@angular/core' 68 | import {AppModuleNgFactory} from './app/app.module.ngfactory' 69 | 70 | if(process.env.NODE_ENV === 'production') { 71 | enableProdMode() 72 | } 73 | 74 | platformBrowser().bootstrapModuleFactory(AppModuleNgFactory) 75 | ``` 76 | 77 | ## Known issues 78 | 79 | - AOT mode is highly experimental 80 | - Lazy-loading does not work in JIT 81 | -------------------------------------------------------------------------------- /src/backend/transformers/remove-decorators.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | import {collectDeepNodes } from './ast-helpers' 4 | import {RemoveNodeOperation, StandardTransform, TransformOperation} from './interfaces' 5 | import {makeTransform} from './make-transform' 6 | 7 | export function removeDecorators(getTypeChecker: () => ts.TypeChecker): ts.TransformerFactory { 8 | const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) { 9 | const ops: TransformOperation[] = [] 10 | 11 | collectDeepNodes(sourceFile, ts.SyntaxKind.Decorator) 12 | .filter(decorator => shouldRemove(decorator, getTypeChecker())) 13 | .forEach(decorator => 14 | // Remove the decorator node. 15 | ops.push(new RemoveNodeOperation(sourceFile, decorator)) 16 | ) 17 | 18 | return ops 19 | } 20 | 21 | return makeTransform(standardTransform, getTypeChecker) 22 | } 23 | 24 | function shouldRemove(decorator: ts.Decorator, typeChecker: ts.TypeChecker): boolean { 25 | const origin = getDecoratorOrigin(decorator, typeChecker) 26 | 27 | if(!origin) { 28 | return false 29 | } 30 | 31 | return origin.module === '@angular/core' 32 | } 33 | 34 | // Decorator helpers. 35 | interface DecoratorOrigin { 36 | name: string 37 | module: string 38 | } 39 | 40 | function getDecoratorOrigin( 41 | decorator: ts.Decorator, 42 | typeChecker: ts.TypeChecker 43 | ): DecoratorOrigin | null { 44 | if(!ts.isCallExpression(decorator.expression)) { 45 | return null 46 | } 47 | 48 | let identifier: ts.Node 49 | let name: string|null = null 50 | 51 | if (ts.isPropertyAccessExpression(decorator.expression.expression)) { 52 | identifier = decorator.expression.expression.expression 53 | name = decorator.expression.expression.name.text 54 | } 55 | else if (ts.isIdentifier(decorator.expression.expression)) { 56 | identifier = decorator.expression.expression 57 | } 58 | else { 59 | return null 60 | } 61 | 62 | // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal 63 | const symbol = typeChecker.getSymbolAtLocation(identifier) 64 | 65 | if(symbol && symbol.declarations && symbol.declarations.length > 0) { 66 | const declaration = symbol.declarations[0] 67 | let module: string 68 | 69 | if(ts.isImportSpecifier(declaration)) { 70 | name = (declaration.propertyName || declaration.name).text 71 | module = (declaration.parent!.parent!.parent!.moduleSpecifier as ts.StringLiteral).text 72 | } 73 | else if (ts.isNamespaceImport(declaration)) { 74 | // Use the name from the decorator namespace property access 75 | module = (declaration.parent!.parent!.moduleSpecifier as ts.StringLiteral).text 76 | } 77 | else if (ts.isImportClause(declaration)) { 78 | name = declaration.name!.text 79 | module = (declaration.parent!.moduleSpecifier as ts.StringLiteral).text 80 | } 81 | else { 82 | return null 83 | } 84 | 85 | if(!name) { 86 | return null 87 | } 88 | 89 | return {name, module} 90 | } 91 | 92 | return null 93 | } 94 | -------------------------------------------------------------------------------- /src/backend/ngc/refactor.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as ts from 'typescript' 3 | 4 | /** 5 | * Find all nodes from the AST in the subtree of node of SyntaxKind kind. 6 | * @param node The root node to check, or null if the whole tree should be searched. 7 | * @param sourceFile The source file where the node is. 8 | * @param kind The kind of nodes to find. 9 | * @param recursive Whether to go in matched nodes to keep matching. 10 | * @param max The maximum number of items to return. 11 | * @return all nodes of kind, or [] if none is found 12 | */ 13 | // TODO: replace this with collectDeepNodes and add limits to collectDeepNodes 14 | export function findAstNodes( 15 | node: ts.Node | null, 16 | sourceFile: ts.SourceFile, 17 | kind: ts.SyntaxKind, 18 | recursive = false, 19 | max = Infinity 20 | ): T[] { 21 | // TODO: refactor operations that only need `refactor.findAstNodes()` to use this instead. 22 | if(max === 0) { 23 | return [] 24 | } 25 | if(!node) { 26 | node = sourceFile 27 | } 28 | 29 | const arr: T[] = [] 30 | 31 | if(node.kind === kind) { 32 | // If we're not recursively looking for children, stop here. 33 | if(!recursive) { 34 | return [node as T] 35 | } 36 | 37 | arr.push(node as T) 38 | max-- 39 | } 40 | 41 | if(max > 0) { 42 | for(const child of node.getChildren(sourceFile)) { 43 | findAstNodes(child, sourceFile, kind, recursive, max).forEach((astNode: ts.Node) => { 44 | if(max > 0) { 45 | arr.push(astNode as T) 46 | } 47 | max-- 48 | }) 49 | 50 | if(max <= 0) { 51 | break 52 | } 53 | } 54 | } 55 | return arr 56 | } 57 | 58 | function resolve(filePath: string, program: ts.Program) { 59 | if(path.isAbsolute(filePath)) { 60 | return filePath 61 | } 62 | 63 | const compilerOptions = program.getCompilerOptions() 64 | const basePath = compilerOptions.baseUrl || compilerOptions.rootDir 65 | 66 | if(!basePath) { 67 | throw new Error(`Trying to resolve '${filePath}' without a basePath.`) 68 | } 69 | 70 | return path.join(basePath, filePath) 71 | } 72 | 73 | export class TypeScriptFileRefactor { 74 | public readonly fileName: string 75 | public readonly sourceFile: ts.SourceFile 76 | 77 | constructor(fileName: string, host: ts.CompilerHost, program?: ts.Program, source?: string | null) { 78 | fileName = resolve(fileName, program!).replace(/\\/g, '/') 79 | 80 | this.fileName = fileName 81 | 82 | if(program) { 83 | if(source) { 84 | this.sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true) 85 | } 86 | else { 87 | this.sourceFile = program.getSourceFile(fileName) 88 | } 89 | } 90 | 91 | if(!this.sourceFile) { 92 | this.sourceFile = ts.createSourceFile( 93 | fileName, source || host.readFile(fileName)!, ts.ScriptTarget.Latest, true 94 | ) 95 | } 96 | } 97 | 98 | /** 99 | * Find all nodes from the AST in the subtree of node of SyntaxKind kind. 100 | * @param node The root node to check, or null if the whole tree should be searched. 101 | * @param kind The kind of nodes to find. 102 | * @param recursive Whether to go in matched nodes to keep matching. 103 | * @param max The maximum number of items to return. 104 | * @return all nodes of kind, or [] if none is found 105 | */ 106 | public findAstNodes(node: ts.Node | null, kind: ts.SyntaxKind, recursive = false, max = Infinity): ts.Node[] { 107 | return findAstNodes(node, this.sourceFile, kind, recursive, max) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/backend/transformers/replace-bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {dirname, relative} from 'path' 2 | 3 | import * as ts from 'typescript' 4 | 5 | import {collectDeepNodes} from './ast-helpers' 6 | import {insertStarImport} from './insert-import' 7 | import {ReplaceNodeOperation, StandardTransform, TransformOperation} from './interfaces' 8 | import {makeTransform} from './make-transform' 9 | 10 | export function replaceBootstrap( 11 | shouldTransform: (fileName: string) => boolean, 12 | getEntryModule: () => { path: string, className: string }, 13 | getTypeChecker: () => ts.TypeChecker, 14 | ): ts.TransformerFactory { 15 | const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) { 16 | const ops: TransformOperation[] = [] 17 | const entryModule = getEntryModule() 18 | 19 | if(!shouldTransform(sourceFile.fileName)) { 20 | return ops 21 | } 22 | 23 | // Find all identifiers. 24 | const entryModuleIdentifiers = collectDeepNodes(sourceFile, ts.SyntaxKind.Identifier) 25 | .filter(identifier => identifier.text === entryModule.className) 26 | 27 | if(entryModuleIdentifiers.length === 0) { 28 | return [] 29 | } 30 | 31 | const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path) 32 | const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/') 33 | 34 | // Find the bootstrap calls. 35 | entryModuleIdentifiers.forEach(entryModuleIdentifier => { 36 | // Figure out if it's a `platformBrowserDynamic().bootstrapModule(AppModule)` call. 37 | if(!( 38 | entryModuleIdentifier.parent && entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression 39 | )) { 40 | return 41 | } 42 | 43 | const callExpr = entryModuleIdentifier.parent as ts.CallExpression 44 | 45 | if(callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { 46 | return 47 | } 48 | 49 | const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression 50 | 51 | if( 52 | propAccessExpr.name.text !== 'bootstrapModule' || propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression 53 | ) { 54 | return 55 | } 56 | 57 | const bootstrapModuleIdentifier = propAccessExpr.name 58 | const innerCallExpr = propAccessExpr.expression as ts.CallExpression 59 | 60 | if(!( 61 | innerCallExpr.expression.kind === ts.SyntaxKind.Identifier 62 | && (innerCallExpr.expression as ts.Identifier).text === 'platformBrowserDynamic' 63 | )) { 64 | return 65 | } 66 | 67 | const platformBrowserDynamicIdentifier = innerCallExpr.expression as ts.Identifier 68 | 69 | const idPlatformBrowser = ts.createUniqueName('__NgCli_bootstrap_') 70 | const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_') 71 | 72 | // Add the transform operations. 73 | const factoryClassName = entryModule.className + 'NgFactory' 74 | const factoryModulePath = normalizedEntryModulePath + '.ngfactory' 75 | ops.push( 76 | // Replace the entry module import. 77 | ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), 78 | new ReplaceNodeOperation( 79 | sourceFile, entryModuleIdentifier, ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName)) 80 | ), 81 | // Replace the platformBrowserDynamic import. 82 | ...insertStarImport(sourceFile, idPlatformBrowser, '@angular/platform-browser'), 83 | new ReplaceNodeOperation( 84 | sourceFile, platformBrowserDynamicIdentifier, ts.createPropertyAccess(idPlatformBrowser, 'platformBrowser') 85 | ), 86 | new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, ts.createIdentifier('bootstrapModuleFactory')) 87 | ) 88 | }) 89 | 90 | return ops 91 | } 92 | 93 | return makeTransform(standardTransform, getTypeChecker) 94 | } 95 | -------------------------------------------------------------------------------- /src/frontend/assets/jit.ts: -------------------------------------------------------------------------------- 1 | import JSAsset = require('parcel-bundler/lib/assets/JSAsset') 2 | 3 | import {Configuration, loadConfiguration, Transpiler} from 'parcel-plugin-typescript/exports' 4 | 5 | import {replaceResources} from '../../backend/transformers/resources' 6 | import {IPCClient} from '../../backend/worker/client' 7 | import {Resources} from '../../interfaces' 8 | import {getFileResources, processResource} from '../loaders/template' 9 | import {collectDependencies} from '../utils/collect-dependencies' 10 | 11 | declare global { 12 | interface PreProcessor { 13 | findExpr: RegExp 14 | replaceExpr: RegExp 15 | transform: (path: string) => Promise 16 | } 17 | } 18 | 19 | export = class TSAsset extends JSAsset { 20 | private readonly config: Promise 21 | private readonly transpiler: Promise 22 | private readonly resources: Resources = { 23 | bundled: [], 24 | external: [] 25 | } 26 | 27 | private readonly templatePreProcessor: PreProcessor = { 28 | findExpr: /_PRAGMA_PARCEL_TYPESCRIPT_PLUGIN_PREPROCESS_TEMPLATE\(([^\)]*)\)/g, 29 | replaceExpr: /._PRAGMA_PARCEL_TYPESCRIPT_PLUGIN_PREPROCESS_TEMPLATE\(([^\)]*)\)./g, 30 | transform: path => processResource(path, this.package, this.options, this.options.parser) 31 | } 32 | private readonly stylePreProcessor: PreProcessor = { 33 | findExpr: /_PRAGMA_PARCEL_TYPESCRIPT_PLUGIN_PREPROCESS_STYLE\(([^\)]*)\)/g, 34 | replaceExpr: /._PRAGMA_PARCEL_TYPESCRIPT_PLUGIN_PREPROCESS_STYLE\(([^\)]*)\)./g, 35 | // TODO: preprocess CSS 36 | transform: path => processResource(path, this.package, this.options, this.options.parser) 37 | } 38 | 39 | constructor(name: string, pkg: string, options: any) { 40 | super(name, pkg, options) 41 | 42 | this.config = loadConfiguration(name) 43 | this.transpiler = this.config.then(config => new Transpiler(config, [replaceResources(() => true)])) 44 | } 45 | 46 | public async parse(code: string) { 47 | const {path: tsConfig} = await this.config 48 | 49 | IPCClient.typeCheck({tsConfig, file: this.name}) 50 | 51 | const transpiler = await this.transpiler 52 | const {sources} = transpiler.transpile(code, this.name) 53 | 54 | this.resources.bundled.splice(0) 55 | this.resources.external.splice(0) 56 | 57 | this.contents = await this.preProcessResources(sources.js, this.templatePreProcessor, this.stylePreProcessor) 58 | 59 | // Parse result as ast format through babylon 60 | return super.parse(this.contents) 61 | } 62 | 63 | public collectDependencies() { 64 | super.collectDependencies() 65 | 66 | collectDependencies(this, this.resources) 67 | } 68 | 69 | private async preProcessResources(code: string, ...preProcessors: PreProcessor[]): Promise { 70 | const results = preProcessors 71 | .map(preProcessor => ({ 72 | matches: code.match(preProcessor.findExpr) || [], 73 | preProcessor 74 | })) 75 | 76 | const resources: {[key: string]: string} = {} 77 | 78 | await Promise.all( 79 | results 80 | .map(({matches, preProcessor}) => 81 | matches.map(match => { 82 | const found = preProcessor.findExpr.exec(match) 83 | 84 | if(found) { 85 | return found.pop() 86 | } 87 | }) 88 | .filter(match => !!match) 89 | .map(async match => { 90 | const path = new Buffer(match!, 'base64').toString('utf-8') 91 | 92 | resources[match!] = await preProcessor.transform(path) 93 | 94 | this.resources.external.push(...getFileResources(path)) 95 | this.resources.bundled.push(path) 96 | }) 97 | ) 98 | .reduce((a, b) => a.concat(b), []) 99 | ) 100 | 101 | results.forEach(({preProcessor}) => 102 | code = code.replace(preProcessor.replaceExpr, (_, path) => 103 | JSON.stringify(resources[path]) 104 | ) 105 | ) 106 | 107 | return code 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/backend/worker/index.ts: -------------------------------------------------------------------------------- 1 | import {HandlerMethod, readFile, Server, setSocketPath, Worker} from 'parcel-plugin-typescript/exports' 2 | 3 | import { 4 | CompileRequest, CompileResult, 5 | ServerRequest, ServerResponse, WorkerRequest, WorkerResponse 6 | } from '../../interfaces' 7 | 8 | import {getFileResources, processResource} from '../../frontend/loaders/template' 9 | import {getWatcher} from '../../utils/get-watcher' 10 | 11 | export class AngularWorker extends Worker { 12 | public readonly resources: Map 13 | private readonly bundler: any 14 | 15 | constructor(bundler: any) { 16 | // We append the socket path to process.env beforce spawning the worker 17 | setSocketPath('angular') 18 | 19 | super(require.resolve('./launcher')) 20 | 21 | this.bundler = bundler 22 | this.resources = new Map() 23 | } 24 | 25 | public async getFactories(file: string): Promise { 26 | try { 27 | const factories = await this.request('getFactories', undefined) 28 | const base = file.replace(/\.tsx?/, '') 29 | 30 | return factories.filter(factory => factory.indexOf(base) === 0) 31 | } 32 | catch { 33 | return [] 34 | } 35 | } 36 | 37 | @HandlerMethod 38 | public async compile(data: CompileRequest): Promise { 39 | const {resources, sources} = await this.request('compile', data) 40 | const result = { 41 | sources, 42 | resources: { 43 | bundled: resources, 44 | external: resources 45 | .map(resource => getFileResources(resource)) 46 | .reduce((a, b) => a.concat(b), []) 47 | } 48 | } 49 | 50 | for(const dep of resources.concat(result.resources.external)) { 51 | const deps = this.resources.get(dep) 52 | 53 | if(!deps) { 54 | this.resources.set(dep, [data.file]) 55 | } 56 | else { 57 | deps.push(data.file) 58 | } 59 | } 60 | 61 | return result 62 | } 63 | 64 | @HandlerMethod 65 | public typeCheck(data: CompileRequest): Promise { 66 | return this.request('typeCheck', data) 67 | } 68 | 69 | @HandlerMethod 70 | public readVirtualFile(file: string) { 71 | return this.request('readVirtualFile', file) 72 | } 73 | 74 | @HandlerMethod 75 | public async processResource(file: string) { 76 | const {package: pkg, options, parser} = this.bundler 77 | 78 | const source = await readFile(file) 79 | 80 | return processResource(file, pkg, options, parser, source) 81 | } 82 | 83 | @HandlerMethod 84 | public invalidate(files: string[]) { 85 | return this.request('invalidate', files) 86 | } 87 | } 88 | 89 | export class AngularServer extends Server { 90 | private readonly worker: AngularWorker 91 | 92 | constructor(bundler: any) { 93 | const worker = new AngularWorker(bundler) 94 | 95 | super('angular', worker) 96 | 97 | this.worker = worker 98 | 99 | this.watch(bundler) 100 | } 101 | 102 | public close() { 103 | this.worker.kill() 104 | 105 | super.close() 106 | } 107 | 108 | private async watch(bundler: any) { 109 | const watcher = await getWatcher(bundler) 110 | 111 | if(!watcher) { 112 | return 113 | } 114 | 115 | const {worker} = this 116 | 117 | watcher.on('change', async (file: string) => { 118 | const deps = worker.resources.get(file) 119 | const files: string[] = [] 120 | 121 | worker.invalidate([file]) 122 | 123 | if(deps) { 124 | files.push(...deps) 125 | } 126 | 127 | files.forEach(depFile => bundler.onChange(depFile)) 128 | 129 | const factories = await Promise.all( 130 | files.map(depFile => worker.getFactories(depFile)) 131 | ) 132 | 133 | factories 134 | .reduce((a, b) => a.concat(b), []) 135 | .forEach(factory => bundler.onChange(factory)) 136 | 137 | // TODO: batch this 138 | worker.invalidate(files) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/backend/transformers/insert-import.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | import {collectDeepNodes, getFirstNode} from './ast-helpers' 4 | import {AddNodeOperation, TransformOperation} from './interfaces' 5 | 6 | export function insertStarImport( 7 | sourceFile: ts.SourceFile, 8 | identifier: ts.Identifier, 9 | modulePath: string, 10 | target?: ts.Node, 11 | before = false, 12 | ): TransformOperation[] { 13 | const ops: TransformOperation[] = [] 14 | const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration) 15 | 16 | // We don't need to verify if the symbol is already imported, star imports should be unique. 17 | 18 | // Create the new import node. 19 | const namespaceImport = ts.createNamespaceImport(identifier) 20 | const importClause = ts.createImportClause(undefined, namespaceImport) 21 | const newImport = ts.createImportDeclaration(undefined, undefined, importClause, 22 | ts.createLiteral(modulePath)) 23 | 24 | if(target) { 25 | ops.push(new AddNodeOperation( 26 | sourceFile, 27 | target, 28 | before ? newImport : undefined, 29 | before ? undefined : newImport 30 | )) 31 | } 32 | else if(allImports.length > 0) { 33 | // Find the last import and insert after. 34 | ops.push(new AddNodeOperation( 35 | sourceFile, 36 | allImports[allImports.length - 1], 37 | undefined, 38 | newImport 39 | )) 40 | } 41 | else { 42 | const node = getFirstNode(sourceFile) 43 | 44 | if(node) { 45 | // Insert before the first node. 46 | ops.push(new AddNodeOperation(sourceFile, node, newImport)) 47 | 48 | } 49 | } 50 | 51 | return ops 52 | } 53 | 54 | export function insertImport( 55 | sourceFile: ts.SourceFile, 56 | symbolName: string, 57 | modulePath: string 58 | ): TransformOperation[] { 59 | const ops: TransformOperation[] = [] 60 | // Find all imports. 61 | const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration) 62 | const maybeImports = (allImports as ts.ImportDeclaration[]) 63 | .filter(node => { 64 | // Filter all imports that do not match the modulePath. 65 | return ( 66 | node.kind === ts.SyntaxKind.ImportDeclaration && 67 | node.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral && 68 | (node.moduleSpecifier as ts.StringLiteral).text === modulePath 69 | ) 70 | }) 71 | .filter(node => { 72 | // Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. 73 | const clause = node.importClause as ts.ImportClause 74 | 75 | if(!clause || clause.name || !clause.namedBindings) { 76 | return false 77 | } 78 | 79 | return clause.namedBindings.kind === ts.SyntaxKind.NamedImports 80 | }) 81 | .map(node => { 82 | // Return the `{ ... }` list of the named import. 83 | return (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports 84 | }) 85 | 86 | if(maybeImports.length) { 87 | // There's an `import {A, B, C} from 'modulePath'`. 88 | // Find if it's in either imports. If so, just return nothing to do. 89 | const hasImportAlready = maybeImports.some(node => 90 | node.elements.some(element => element.name.text === symbolName) 91 | ) 92 | 93 | if(hasImportAlready) { 94 | return ops 95 | } 96 | 97 | // Just pick the first one and insert at the end of its identifier list. 98 | ops.push(new AddNodeOperation( 99 | sourceFile, 100 | maybeImports[0].elements[maybeImports[0].elements.length - 1], 101 | undefined, 102 | ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName)) 103 | )) 104 | } else { 105 | // Create the new import node. 106 | const namedImports = ts.createNamedImports([ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName))]) 107 | const importClause = ts.createImportClause(undefined, namedImports) 108 | const newImport = ts.createImportDeclaration(undefined, undefined, importClause, ts.createLiteral(modulePath)) 109 | 110 | if(allImports.length > 0) { 111 | // Find the last import and insert after. 112 | ops.push(new AddNodeOperation( 113 | sourceFile, 114 | allImports[allImports.length - 1], 115 | undefined, 116 | newImport 117 | )) 118 | } 119 | else { 120 | const node = getFirstNode(sourceFile) 121 | 122 | if(node) { 123 | // Insert before the first node. 124 | ops.push(new AddNodeOperation(sourceFile, node, newImport)) 125 | } 126 | } 127 | } 128 | 129 | return ops 130 | } 131 | -------------------------------------------------------------------------------- /src/backend/format.ts: -------------------------------------------------------------------------------- 1 | import {EOL} from 'os' 2 | 3 | import {Diagnostic} from '@angular/compiler-cli/src/transformers/api' 4 | import {codeFrameColumns, Location} from '@babel/code-frame' 5 | 6 | import chalk from 'chalk' 7 | import lineColumn = require('line-column') 8 | import normalizePath = require('normalize-path') 9 | import * as ts from 'typescript' 10 | 11 | export function formatDiagnostics(diagnostics: Array, context: string): string { 12 | const diags = diagnostics.map(diagnostic => { 13 | if(diagnostic.source === 'angular') { 14 | return formatAngularDiagnostic(diagnostic as Diagnostic, context) 15 | } 16 | else { 17 | return formatTypeScriptDiagnostic(diagnostic as ts.Diagnostic, context) 18 | } 19 | }) 20 | 21 | if(diags.length > 0) { 22 | return diags.join(EOL) + EOL 23 | } 24 | else { 25 | return '' 26 | } 27 | } 28 | 29 | function formatTypeScriptDiagnostic(diagnostic: ts.Diagnostic, context: string) { 30 | const messageText = formatDiagnosticMessage(diagnostic.messageText, '', context) 31 | const {file} = diagnostic 32 | let message = messageText 33 | 34 | if(file != null && diagnostic.start != null) { 35 | const lineChar = file.getLineAndCharacterOfPosition(diagnostic.start) 36 | const source = file.text || diagnostic.source 37 | const start = { 38 | line: lineChar.line + 1, 39 | column: lineChar.character + 1 40 | } 41 | const location: Location = {start} 42 | const red = chalk.red(`🚨 ${file.fileName}(${start.line},${start.column})`) 43 | 44 | const messages = [`${red}\n${chalk.redBright(messageText)}`] 45 | 46 | if(source != null) { 47 | if(typeof diagnostic.length === 'number') { 48 | const end = file.getLineAndCharacterOfPosition(diagnostic.start + diagnostic.length) 49 | 50 | location.end = { 51 | line: end.line + 1, 52 | column: end.character + 1 53 | } 54 | } 55 | 56 | const frame = codeFrameColumns(source, location, { 57 | linesAbove: 1, 58 | linesBelow: 1, 59 | highlightCode: true 60 | }) 61 | 62 | messages.push( 63 | frame 64 | .split('\n') 65 | .map(str => ` ${str}`) 66 | .join('\n') 67 | ) 68 | } 69 | 70 | message = messages.join('\n') 71 | } 72 | 73 | return message + EOL 74 | } 75 | 76 | function formatAngularDiagnostic(diag: Diagnostic, context: string) { 77 | const diagnostic = diag as Diagnostic & { 78 | file?: {text: string, fileName: string} 79 | start?: number 80 | length?: number 81 | } 82 | const messageText = formatDiagnosticMessage(diagnostic.messageText, '', context) 83 | const {file, span} = diagnostic 84 | 85 | interface LineColumn {line: number, col: number} 86 | let fileName: string|null = null 87 | let source: string|null = null 88 | let start: LineColumn|null = null 89 | let end: LineColumn|null = null 90 | 91 | if(file && typeof diagnostic.start === 'number' && typeof diagnostic.length === 'number') { 92 | source = file.text 93 | fileName = file.fileName 94 | start = lineColumn(source, diagnostic.start) 95 | end = lineColumn(source, diagnostic.start + diagnostic.length) 96 | } 97 | else if(span) { 98 | source = span.start.file.content 99 | fileName = span.start.file.url 100 | start = span.start 101 | end = span.end 102 | 103 | start.line++ 104 | start.col++ 105 | end.line++ 106 | end.col++ 107 | } 108 | else { 109 | return 110 | } 111 | 112 | const location: Location = { 113 | start: { 114 | line: start.line , 115 | column: start.col 116 | }, 117 | end: { 118 | line: end.line, 119 | column: end.col 120 | } 121 | } 122 | const red = chalk.red(`🚨 ${fileName}(${start.line},${start.col})`) 123 | 124 | const messages = [`${red}\n${chalk.redBright(messageText)}`] 125 | 126 | const frame = codeFrameColumns(source, location, { 127 | linesAbove: 1, 128 | linesBelow: 1, 129 | highlightCode: true 130 | }) 131 | 132 | messages.push( 133 | frame 134 | .split('\n') 135 | .map(str => ` ${str}`) 136 | .join('\n') 137 | ) 138 | 139 | return messages.join('\n') + EOL 140 | } 141 | 142 | function replaceAbsolutePaths(message: string, context: string) { 143 | const contextPath = normalizePath(context) 144 | 145 | return message.replace(new RegExp(contextPath, 'g'), '.') 146 | } 147 | 148 | function formatDiagnosticMessage(diagnostic: string|ts.DiagnosticMessageChain, delimiter: string, context: string) { 149 | return replaceAbsolutePaths(ts.flattenDiagnosticMessageText(diagnostic, delimiter), context) 150 | } 151 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [0.5.0](https://github.com/fathyb/parcel-plugin-angular/compare/v0.4.0...v0.5.0) (2018-01-06) 7 | 8 | 9 | ### Features 10 | 11 | * **reporter:** add Angular error reporter ([7ec561c](https://github.com/fathyb/parcel-plugin-angular/commit/7ec561c)) 12 | 13 | 14 | 15 | 16 | # [0.4.0](https://github.com/fathyb/parcel-plugin-angular/compare/v0.2.4...v0.4.0) (2018-01-05) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | 🎉 First Angular AOT support 🎉 22 | 23 | ### Features 24 | 25 | - **Angular** 26 | - Compilation using AOT compiler 27 | - Support lazy-loading (AOT only) 28 | - Preprocess templates and style using Parcel (`templateUrl` or `styleUrls` only) 29 | - Experimental incremental AOT build on watch mode 30 | - Decorators are removed in AOT for smaller builds 31 | - **Options**: you can now pass options to the plugin in `tsconfig.json`: 32 | ```js 33 | { 34 | "compilerOptions": {...}, 35 | // the plugin options 36 | "parcelAngularOptions": { 37 | // What compiler should we use when watching or serving 38 | "watch": "jit", 39 | 40 | // What compiler should we use when building (parcel build) 41 | "build": "aot" 42 | } 43 | } 44 | ``` 45 | 46 | 47 | ## [0.2.5](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.2.4...v0.2.5) (2017-12-21) 48 | 49 | ### Bug Fixes 50 | 51 | * **resolve:** correctly map directory indices ([a543347](https://github.com/fathyb/parcel-plugin-typescript/commit/a543347)) 52 | 53 | 54 | 55 | ## [0.2.4](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.2.3...v0.2.4) (2017-12-19) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * **mappings:** check baseUrl before transform ([10863f1](https://github.com/fathyb/parcel-plugin-typescript/commit/10863f1)) 61 | 62 | 63 | 64 | 65 | ## [0.2.3](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.2.2...v0.2.3) (2017-12-15) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **mappings:** fix es2015 import transformations ([bc9e6a4](https://github.com/fathyb/parcel-plugin-typescript/commit/bc9e6a4)) 71 | * **resolve:** default moduleResolution to node ([b0d111d](https://github.com/fathyb/parcel-plugin-typescript/commit/b0d111d)) 72 | 73 | 74 | 75 | 76 | ## [0.2.2](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.2.1...v0.2.2) (2017-12-15) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * **logs:** do not log empty lines ([b71da83](https://github.com/fathyb/parcel-plugin-typescript/commit/b71da83)) 82 | * **mappings:** resolve when path is undefined ([1361c83](https://github.com/fathyb/parcel-plugin-typescript/commit/1361c83)) 83 | 84 | 85 | 86 | 87 | ## [0.2.1](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.2.0...v0.2.1) (2017-12-13) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **build:** prevent hanging when only building ([5465994](https://github.com/fathyb/parcel-plugin-typescript/commit/5465994)) 93 | 94 | 95 | 96 | 97 | # [0.2.0](https://github.com/fathyb/parcel-plugin-typescript/compare/v0.1.0...v0.2.0) (2017-12-12) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * **type-check:** correctly load user tsconfig ([24900cb](https://github.com/fathyb/parcel-plugin-typescript/commit/24900cb)) 103 | 104 | 105 | ### Features 106 | 107 | * **transpiler:** add Angular AST transform support ([22a040e](https://github.com/fathyb/parcel-plugin-typescript/commit/22a040e)) 108 | * **transpiler:** support custom mappings ([5a550ce](https://github.com/fathyb/parcel-plugin-typescript/commit/5a550ce)) 109 | 110 | 111 | 112 | 113 | # 0.1.0 (2017-12-10) 114 | 115 | 116 | ### Features 117 | 118 | * fork type-checker to separate process ([5a18d78](https://github.com/fathyb/parcel-plugin-typescript/commit/5a18d78)) 119 | * **error-report:** add error underlining ([24b70c9](https://github.com/fathyb/parcel-plugin-typescript/commit/24b70c9)) 120 | * **type-check:** implement incremental build ([fd771e8](https://github.com/fathyb/parcel-plugin-typescript/commit/fd771e8)) 121 | 122 | 123 | ### Performance Improvements 124 | 125 | * use type-checker to transpile on main thread ([d41d95f](https://github.com/fathyb/parcel-plugin-typescript/commit/d41d95f)) 126 | -------------------------------------------------------------------------------- /src/backend/transformers/elide-imports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | import {collectDeepNodes } from './ast-helpers' 4 | import {RemoveNodeOperation, TransformOperation} from './interfaces' 5 | 6 | interface RemovedSymbol { 7 | symbol: ts.Symbol 8 | importDecl: ts.ImportDeclaration 9 | importSpec: ts.ImportSpecifier 10 | singleImport: boolean 11 | removed: ts.Identifier[] 12 | all: ts.Identifier[] 13 | } 14 | 15 | // Remove imports for which all identifiers have been removed. 16 | // Needs type checker, and works even if it's not the first transformer. 17 | // Works by removing imports for symbols whose identifiers have all been removed. 18 | // Doesn't use the `symbol.declarations` because that previous transforms might have removed nodes 19 | // but the type checker doesn't know. 20 | // See https://github.com/Microsoft/TypeScript/issues/17552 for more information. 21 | export function elideImports( 22 | sourceFile: ts.SourceFile, 23 | removedNodes: ts.Node[], 24 | getTypeChecker: () => ts.TypeChecker, 25 | ): TransformOperation[] { 26 | const ops: TransformOperation[] = [] 27 | 28 | if(removedNodes.length === 0) { 29 | return [] 30 | } 31 | 32 | // Get all children identifiers inside the removed nodes. 33 | const removedIdentifiers = removedNodes 34 | .map(node => collectDeepNodes(node, ts.SyntaxKind.Identifier)) 35 | .reduce((prev, curr) => prev.concat(curr), []) 36 | // Also add the top level nodes themselves if they are identifiers. 37 | .concat(removedNodes.filter(node => node.kind === ts.SyntaxKind.Identifier) as ts.Identifier[]) 38 | 39 | if(removedIdentifiers.length === 0) { 40 | return [] 41 | } 42 | 43 | // Get all imports in the source file. 44 | const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration) 45 | 46 | if(allImports.length === 0) { 47 | return [] 48 | } 49 | 50 | const removedSymbolMap: Map = new Map() 51 | const typeChecker = getTypeChecker() 52 | 53 | // Find all imports that use a removed identifier and add them to the map. 54 | allImports 55 | .filter((node: ts.ImportDeclaration) => { 56 | // TODO: try to support removing `import * as X from 'XYZ'`. 57 | // Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. 58 | const clause = node.importClause as ts.ImportClause 59 | 60 | if(!clause || clause.name || !clause.namedBindings) { 61 | return false 62 | } 63 | 64 | return clause.namedBindings.kind === ts.SyntaxKind.NamedImports 65 | }) 66 | .forEach((importDecl: ts.ImportDeclaration) => { 67 | const importClause = importDecl.importClause as ts.ImportClause 68 | const namedImports = importClause.namedBindings as ts.NamedImports 69 | 70 | namedImports.elements.forEach((importSpec: ts.ImportSpecifier) => { 71 | const importId = importSpec.name 72 | const symbol = typeChecker.getSymbolAtLocation(importId) 73 | 74 | const removedNodesForImportId = removedIdentifiers.filter(id => 75 | id.text === importId.text && typeChecker.getSymbolAtLocation(id) === symbol) 76 | 77 | if(symbol && removedNodesForImportId.length > 0) { 78 | removedSymbolMap.set(importId.text, { 79 | symbol, 80 | importDecl, 81 | importSpec, 82 | singleImport: namedImports.elements.length === 1, 83 | removed: removedNodesForImportId, 84 | all: [] 85 | }) 86 | } 87 | }) 88 | }) 89 | 90 | if(removedSymbolMap.size === 0) { 91 | return [] 92 | } 93 | 94 | // Find all identifiers in the source file that have a removed symbol, and add them to the map. 95 | collectDeepNodes(sourceFile, ts.SyntaxKind.Identifier) 96 | .forEach(id => { 97 | if(removedSymbolMap.has(id.text)) { 98 | const symbol = removedSymbolMap.get(id.text) 99 | 100 | if(symbol && typeChecker.getSymbolAtLocation(id) === symbol.symbol) { 101 | symbol.all.push(id) 102 | } 103 | } 104 | }) 105 | 106 | Array 107 | .from(removedSymbolMap.values()) 108 | .filter(symbol => { 109 | // If the number of removed imports plus one (the import specifier) is equal to the total 110 | // number of identifiers for that symbol, it's safe to remove the import. 111 | return symbol.removed.length + 1 === symbol.all.length 112 | }) 113 | .forEach(symbol => { 114 | // Remove the whole declaration if it's a single import. 115 | const nodeToRemove = symbol.singleImport ? symbol.importDecl : symbol.importSpec 116 | 117 | ops.push(new RemoveNodeOperation(sourceFile, nodeToRemove)) 118 | }) 119 | 120 | return ops 121 | } 122 | -------------------------------------------------------------------------------- /src/backend/ngc/build-loop.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | import {ParsedConfiguration, readConfiguration} from '@angular/compiler-cli' 4 | import {Diagnostic, EmitFlags, Program} from '@angular/compiler-cli/src/transformers/api' 5 | import {createProgram} from '@angular/compiler-cli/src/transformers/program' 6 | 7 | import {AsyncLoop} from '../../utils/async-loop' 8 | 9 | import {reportDiagnostics} from '../reporter' 10 | import {removeDecorators} from '../transformers/remove-decorators' 11 | import {replaceBootstrap} from '../transformers/replace-bootstrap' 12 | import {findResources} from '../transformers/resources' 13 | 14 | import {resolveEntryModuleFromMain} from './entry-resolver' 15 | import {AngularCompilerHost} from './host' 16 | 17 | export class BuildLoop extends AsyncLoop { 18 | public entryFile: string|null = null 19 | 20 | private readonly resources: {[file: string]: string[]} = {} 21 | private entryModule: {className: string, path: string}|null = null 22 | private program: Program|undefined = undefined 23 | private firstRun = true 24 | private shouldEmit = false 25 | 26 | constructor( 27 | private readonly host: AngularCompilerHost, 28 | private readonly config: ParsedConfiguration, 29 | private readonly transformers: Array> 30 | ) { 31 | super(() => this.compile()) 32 | } 33 | 34 | public getProgram(): Program|undefined { 35 | return this.program 36 | } 37 | 38 | public getResources(file: string): string[] { 39 | return this.resources[file] || [] 40 | } 41 | 42 | private async createProgramIfNeeded() { 43 | const {host} = this 44 | const {changedFiles} = host.store 45 | let {program} = this 46 | 47 | if(changedFiles.length > 0 || this.firstRun || !program) { 48 | const config = readConfiguration(this.config.project) 49 | const {options, rootNames} = config 50 | 51 | program = createProgram({rootNames, options, host, oldProgram: program}) 52 | 53 | this.program = program 54 | this.shouldEmit = true 55 | 56 | const resource = changedFiles.find(file => !/\.ts$/.test(file)) 57 | 58 | if(resource) { 59 | Object.keys(host.store['sources']).forEach(key => { 60 | if(!/node_modules/.test(key)) { 61 | delete host.store['sources'][key] 62 | } 63 | }) 64 | } 65 | 66 | changedFiles.splice(0) 67 | 68 | await program.loadNgStructureAsync() 69 | 70 | this.updateResources(program) 71 | } 72 | 73 | return program 74 | } 75 | 76 | private async compile() { 77 | const program = await this.createProgramIfNeeded() 78 | 79 | if(!this.shouldEmit) { 80 | return 81 | } 82 | 83 | this.shouldEmit = false 84 | 85 | const getTypeChecker = () => program.getTsProgram().getTypeChecker() 86 | const transformers: Array> = [removeDecorators(getTypeChecker)] 87 | const diagnostics: Array = [] 88 | 89 | diagnostics.push(...program.getNgStructuralDiagnostics()) 90 | 91 | if(this.firstRun) { 92 | this.firstRun = false 93 | 94 | diagnostics.push(...program.getNgOptionDiagnostics(), ...program.getTsOptionDiagnostics()) 95 | } 96 | 97 | const {entryFile, host} = this 98 | let {entryModule} = this 99 | 100 | if(!entryModule && entryFile) { 101 | try { 102 | const [path, className = 'default'] = resolveEntryModuleFromMain(entryFile, host, program.getTsProgram()) 103 | .split('#') 104 | 105 | entryModule = {className, path} 106 | this.entryModule = entryModule 107 | } 108 | catch(_) { 109 | entryModule = null 110 | } 111 | } 112 | 113 | if(entryModule) { 114 | // TODO: this has to be improved or removed 115 | transformers.push(replaceBootstrap(file => file === entryFile, () => entryModule!, getTypeChecker)) 116 | } 117 | 118 | const result = program.emit({ 119 | emitFlags: EmitFlags.All, 120 | customTransformers: { 121 | beforeTs: [...this.transformers, ...transformers] 122 | } 123 | }) 124 | 125 | diagnostics.push( 126 | ...program 127 | .getTsSemanticDiagnostics() 128 | // TODO: whyyyyyyyyyy do i have to do this 129 | .filter(diag => 130 | typeof diag.messageText === 'string' 131 | ? !/^Module '"|'[^('|")]+\.ngfactory'|"' has no exported member '[^']+NgFactory'./.test(diag.messageText) 132 | : true 133 | ), 134 | ...program.getTsSyntacticDiagnostics(), 135 | ...program.getNgSemanticDiagnostics(), 136 | ...result.diagnostics 137 | ) 138 | 139 | reportDiagnostics(diagnostics) 140 | } 141 | 142 | private updateResources(program: Program) { 143 | program 144 | .getTsProgram() 145 | .getSourceFiles() 146 | .filter(({text}) => /(templateUrl)|(styleUrls)/.test(text)) 147 | .forEach(sourceFile => 148 | this.resources[sourceFile.fileName] = findResources(sourceFile) 149 | .map(({path}) => typeof path === 'string' ? [path] : path) 150 | .reduce((a, b) => a.concat(b), []) 151 | ) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/backend/transformers/make-transform.ts: -------------------------------------------------------------------------------- 1 | import {satisfies} from 'semver' 2 | import * as ts from 'typescript' 3 | 4 | import {elideImports} from './elide-imports' 5 | import { 6 | AddNodeOperation, 7 | OPERATION_KIND, 8 | RemoveNodeOperation, 9 | ReplaceNodeOperation, 10 | StandardTransform, 11 | TransformOperation, 12 | } from './interfaces' 13 | 14 | // Typescript below 2.5.0 needs a workaround. 15 | const visitEachChild: typeof ts.visitEachChild = satisfies(ts.version, '^2.5.0') 16 | ? ts.visitEachChild 17 | : visitEachChildWorkaround 18 | 19 | export function makeTransform( 20 | standardTransform: StandardTransform, 21 | getTypeChecker?: () => ts.TypeChecker, 22 | ): ts.TransformerFactory { 23 | return (context: ts.TransformationContext): ts.Transformer => { 24 | const transformer: ts.Transformer = (sf: ts.SourceFile) => { 25 | const ops: TransformOperation[] = standardTransform(sf) 26 | const removeOps = ops 27 | .filter(op => op.kind === OPERATION_KIND.Remove) as RemoveNodeOperation[] 28 | const addOps = ops.filter(op => op.kind === OPERATION_KIND.Add) as AddNodeOperation[] 29 | const replaceOps = ops 30 | .filter(op => op.kind === OPERATION_KIND.Replace) as ReplaceNodeOperation[] 31 | 32 | // If nodes are removed, elide the imports as well. 33 | // Mainly a workaround for https://github.com/Microsoft/TypeScript/issues/17552. 34 | // WARNING: this assumes that replaceOps DO NOT reuse any of the nodes they are replacing. 35 | // This is currently true for transforms that use replaceOps (replace_bootstrap and 36 | // replace_resources), but may not be true for new transforms. 37 | if(getTypeChecker && removeOps.length + replaceOps.length > 0) { 38 | const removedNodes = removeOps.concat(replaceOps).map(op => op.target) 39 | removeOps.push(...elideImports(sf, removedNodes, getTypeChecker)) 40 | } 41 | 42 | const visitor: ts.Visitor = node => { 43 | let modified = false 44 | let modifiedNodes = [node] 45 | // Check if node should be dropped. 46 | if(removeOps.find(op => op.target === node)) { 47 | modifiedNodes = [] 48 | modified = true 49 | } 50 | 51 | // Check if node should be replaced (only replaces with first op found). 52 | const replace = replaceOps.find(op => op.target === node) 53 | if(replace) { 54 | modifiedNodes = [replace.replacement] 55 | modified = true 56 | } 57 | 58 | // Check if node should be added to. 59 | const add = addOps.filter(op => op.target === node) 60 | if(add.length > 0) { 61 | modifiedNodes = [ 62 | ...add.filter(op => op.before).map((op => op.before!)), 63 | ...modifiedNodes, 64 | ...add.filter(op => op.after).map((op => op.after!)) 65 | ] 66 | modified = true 67 | } 68 | 69 | // If we changed anything, return modified nodes without visiting further. 70 | if(modified) { 71 | return modifiedNodes 72 | } else { 73 | // Otherwise return node as is and visit children. 74 | return visitEachChild(node, visitor, context) 75 | } 76 | } 77 | 78 | // Don't visit the sourcefile at all if we don't have ops for it. 79 | if(ops.length === 0) { 80 | return sf 81 | } 82 | 83 | const result = ts.visitNode(sf, visitor) 84 | 85 | // If we removed any decorators, we need to clean up the decorator arrays. 86 | if(removeOps.some(op => op.target.kind === ts.SyntaxKind.Decorator)) { 87 | cleanupDecorators(result) 88 | } 89 | 90 | return result 91 | } 92 | 93 | return transformer 94 | } 95 | } 96 | 97 | /** 98 | * This is a version of `ts.visitEachChild` that works that calls our version 99 | * of `updateSourceFileNode`, so that typescript doesn't lose type information 100 | * for property decorators. 101 | * See https://github.com/Microsoft/TypeScript/issues/17384 and 102 | * https://github.com/Microsoft/TypeScript/issues/17551, fixed by 103 | * https://github.com/Microsoft/TypeScript/pull/18051 and released on TS 2.5.0. 104 | * 105 | * @param sf 106 | * @param statements 107 | */ 108 | function visitEachChildWorkaround(node: ts.Node, visitor: ts.Visitor, context: ts.TransformationContext) { 109 | 110 | if(node.kind === ts.SyntaxKind.SourceFile) { 111 | const sf = node as ts.SourceFile 112 | const statements = ts.visitLexicalEnvironment(sf.statements, visitor, context) 113 | 114 | if(statements === sf.statements) { 115 | return sf 116 | } 117 | // Note: Need to clone the original file (and not use `ts.updateSourceFileNode`) 118 | // as otherwise TS fails when resolving types for decorators. 119 | const sfClone = ts.getMutableClone(sf) 120 | sfClone.statements = statements 121 | return sfClone 122 | } 123 | 124 | return ts.visitEachChild(node, visitor, context) 125 | } 126 | 127 | // If TS sees an empty decorator array, it will still emit a `__decorate` call. 128 | // This seems to be a TS bug. 129 | function cleanupDecorators(node: ts.Node) { 130 | if(node.decorators && node.decorators.length === 0) { 131 | node.decorators = undefined 132 | } 133 | 134 | ts.forEachChild(node, child => cleanupDecorators(child)) 135 | } 136 | -------------------------------------------------------------------------------- /src/backend/ngc/entry-resolver.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import {join} from 'path' 3 | import * as ts from 'typescript' 4 | 5 | import {TypeScriptFileRefactor} from './refactor' 6 | 7 | function recursiveSymbolExportLookup( 8 | refactor: TypeScriptFileRefactor, 9 | symbolName: string, 10 | host: ts.CompilerHost, 11 | program: ts.Program 12 | ): string | null { 13 | // Check this file. 14 | const hasSymbol = (refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration[]) 15 | .some(cd => cd.name !== undefined && cd.name.text === symbolName) 16 | 17 | if(hasSymbol) { 18 | return refactor.fileName 19 | } 20 | 21 | // We found the bootstrap variable, now we just need to get where it's imported. 22 | const exports = refactor.findAstNodes(null, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration[] 23 | 24 | for(const decl of exports) { 25 | if(!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { 26 | continue 27 | } 28 | 29 | const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text 30 | const resolvedModule = ts.resolveModuleName(modulePath, refactor.fileName, program.getCompilerOptions(), host) 31 | 32 | if(!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { 33 | return null 34 | } 35 | 36 | const module = resolvedModule.resolvedModule.resolvedFileName 37 | 38 | if(!decl.exportClause) { 39 | const moduleRefactor = new TypeScriptFileRefactor(module, host, program) 40 | const maybeModule = recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program) 41 | 42 | if(maybeModule) { 43 | return maybeModule 44 | } 45 | continue 46 | } 47 | 48 | const binding = decl.exportClause as ts.NamedExports 49 | 50 | for(const specifier of binding.elements) { 51 | if(specifier.name.text === symbolName) { 52 | // If it's a directory, load its index and recursively lookup. 53 | if(fs.statSync(module).isDirectory()) { 54 | const indexModule = join(module, 'index.ts') 55 | 56 | if(fs.existsSync(indexModule)) { 57 | const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program) 58 | const maybeModule = recursiveSymbolExportLookup(indexRefactor, symbolName, host, program) 59 | 60 | if(maybeModule) { 61 | return maybeModule 62 | } 63 | } 64 | } 65 | 66 | // Create the source and verify that the symbol is at least a class. 67 | const source = new TypeScriptFileRefactor(module, host, program) 68 | const valid = (source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration[]) 69 | .some(cd => cd.name !== undefined && cd.name.text === symbolName) 70 | 71 | if(valid) { 72 | return module 73 | } 74 | } 75 | } 76 | } 77 | 78 | return null 79 | } 80 | 81 | function symbolImportLookup( 82 | refactor: TypeScriptFileRefactor, symbolName: string, host: ts.CompilerHost, program: ts.Program 83 | ): string | null { 84 | // We found the bootstrap variable, now we just need to get where it's imported. 85 | const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration) as ts.ImportDeclaration[] 86 | 87 | for(const decl of imports) { 88 | if(!decl.importClause || !decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { 89 | continue 90 | } 91 | 92 | const resolvedModule = ts.resolveModuleName( 93 | (decl.moduleSpecifier as ts.StringLiteral).text, refactor.fileName, program.getCompilerOptions(), host 94 | ) 95 | 96 | if(!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { 97 | continue 98 | } 99 | 100 | const module = resolvedModule.resolvedModule.resolvedFileName 101 | 102 | if(decl.importClause.namedBindings && decl.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { 103 | const binding = decl.importClause.namedBindings as ts.NamespaceImport 104 | 105 | if(binding.name.text === symbolName) { 106 | // This is a default export. 107 | return module 108 | } 109 | } 110 | else if(decl.importClause.namedBindings && decl.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { 111 | const binding = decl.importClause.namedBindings as ts.NamedImports 112 | 113 | for(const specifier of binding.elements) { 114 | if(specifier.name.text === symbolName) { 115 | // Create the source and recursively lookup the import. 116 | const source = new TypeScriptFileRefactor(module, host, program) 117 | const maybeModule = recursiveSymbolExportLookup(source, symbolName, host, program) 118 | 119 | if(maybeModule) { 120 | return maybeModule 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | return null 128 | } 129 | 130 | export function resolveEntryModuleFromMain(mainPath: string, host: ts.CompilerHost, program: ts.Program) { 131 | const source = new TypeScriptFileRefactor(mainPath, host, program) 132 | const bootstrap = 133 | (source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]) 134 | .filter(call => { 135 | const access = call.expression as ts.PropertyAccessExpression 136 | 137 | return access.kind === ts.SyntaxKind.PropertyAccessExpression 138 | && access.name.kind === ts.SyntaxKind.Identifier 139 | && (access.name.text === 'bootstrapModule' || access.name.text === 'bootstrapModuleFactory') 140 | }) 141 | .map(node => node.arguments[0] as ts.Identifier) 142 | .filter(node => node.kind === ts.SyntaxKind.Identifier) 143 | 144 | if(bootstrap.length !== 1) { 145 | throw new Error('Tried to find bootstrap code, but could not. Specify either ' 146 | + 'statically analyzable bootstrap code or pass in an entryModule ' 147 | + 'to the plugins options.') 148 | } 149 | 150 | const bootstrapSymbolName = bootstrap[0].text 151 | const module = symbolImportLookup(source, bootstrapSymbolName, host, program) 152 | 153 | if(module) { 154 | return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}` 155 | } 156 | 157 | // shrug... something bad happened and we couldn't find the import statement. 158 | throw new Error('Tried to find bootstrap code, but could not. Specify either ' 159 | + 'statically analyzable bootstrap code or pass in an entryModule ' 160 | + 'to the plugins options.') 161 | } 162 | -------------------------------------------------------------------------------- /src/backend/transformers/resources.ts: -------------------------------------------------------------------------------- 1 | import {dirname, resolve} from 'path' 2 | import * as ts from 'typescript' 3 | 4 | import {collectDeepNodes, getFirstNode} from './ast-helpers' 5 | import {AddNodeOperation, ReplaceNodeOperation, StandardTransform, TransformOperation} from './interfaces' 6 | import {makeTransform} from './make-transform' 7 | 8 | export interface ResourcesMap { 9 | [path: string]: string 10 | } 11 | 12 | export function replaceResources( 13 | shouldTransform: (fileName: string) => boolean 14 | ): ts.TransformerFactory { 15 | const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) { 16 | const ops: TransformOperation[] = [] 17 | 18 | if(!shouldTransform(sourceFile.fileName)) { 19 | return ops 20 | } 21 | 22 | const resources = findResources(sourceFile) 23 | 24 | if(resources.length > 0) { 25 | // Add the replacement operations. 26 | ops.push(...(resources.map(resource => { 27 | if(resource.type === 'template') { 28 | const propAssign = ts.createPropertyAssignment('template', createPreProcessorNode(resource.path, resource.type)) 29 | 30 | return new ReplaceNodeOperation(sourceFile, resource.node, propAssign) 31 | } 32 | else if(resource.type === 'style') { 33 | const literals = ts.createArrayLiteral( 34 | resource.path.map(path => createPreProcessorNode(path, resource.type)) 35 | ) 36 | 37 | const propAssign = ts.createPropertyAssignment('styles', literals) 38 | 39 | return new ReplaceNodeOperation(sourceFile, resource.node, propAssign) 40 | } 41 | 42 | throw new Error('invariant error') 43 | }))) 44 | 45 | // If we added a require call, we need to also add typings for it. 46 | // The typings need to be compatible with node typings, but also work by themselves. 47 | // interface NodeRequire {(id: string): any;} 48 | const nodeRequireInterface = ts.createInterfaceDeclaration([], [], 'NodeRequire', [], [], [ 49 | ts.createCallSignature([], [ 50 | ts.createParameter([], [], undefined, 'id', undefined, 51 | ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) 52 | ) 53 | ], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)) 54 | ]) 55 | 56 | // declare var require: NodeRequire; 57 | const varRequire = ts.createVariableStatement( 58 | [ts.createToken(ts.SyntaxKind.DeclareKeyword)], 59 | [ts.createVariableDeclaration('require', ts.createTypeReferenceNode('NodeRequire', []))] 60 | ) 61 | 62 | ops.push(new AddNodeOperation(sourceFile, getFirstNode(sourceFile)!, nodeRequireInterface)) 63 | ops.push(new AddNodeOperation(sourceFile, getFirstNode(sourceFile)!, varRequire)) 64 | } 65 | 66 | return ops 67 | } 68 | 69 | return makeTransform(standardTransform) 70 | } 71 | 72 | export interface SourceFileTemplateResource { 73 | type: 'template' 74 | path: string 75 | node: ts.PropertyAssignment 76 | } 77 | export interface SourceFileStyleResource { 78 | type: 'style' 79 | path: string[] 80 | node: ts.PropertyAssignment 81 | } 82 | 83 | export type SourceFileResource = SourceFileTemplateResource | SourceFileStyleResource 84 | 85 | /* 86 | export function findInlineResources(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { 87 | return collectDeepNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression) 88 | .map(node => collectDeepNodes(node, ts.SyntaxKind.PropertyAssignment)) 89 | .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) 90 | .map(node => ({ 91 | node, key: getContentOfKeyLiteral(node.name)! 92 | })) 93 | .filter(({key}) => { 94 | if(!key) { 95 | return false 96 | } 97 | 98 | return key === 'template' || key === 'styles' 99 | }) 100 | .map(({node, key}) => { 101 | if(ts.isExpressionStatement(node)) { 102 | typeChecker.getContextualType(node.expression)!.getProperties()[0].valueDeclaration 103 | } 104 | }) 105 | }*/ 106 | 107 | export function findResources(sourceFile: ts.SourceFile): SourceFileResource[] { 108 | // Find all object literals. 109 | return collectDeepNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression) 110 | // Get all their property assignments. 111 | .map(node => collectDeepNodes(node, ts.SyntaxKind.PropertyAssignment)) 112 | // Flatten into a single array (from an array of array). 113 | .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) 114 | // We only want property assignments for the templateUrl/styleUrls keys. 115 | .filter((node: ts.PropertyAssignment) => { 116 | const key = getContentOfKeyLiteral(node.name) 117 | 118 | if(!key) { 119 | // key is an expression, can't do anything. 120 | return false 121 | } 122 | 123 | return key === 'templateUrl' || key === 'styleUrls' 124 | }) 125 | .map((node): SourceFileResource|undefined => { 126 | const key = getContentOfKeyLiteral(node.name) 127 | 128 | if(key === 'templateUrl') { 129 | const path = getResourceRequest(node.initializer, sourceFile) 130 | 131 | return {path, node, type: 'template'} 132 | } 133 | else if(key === 'styleUrls') { 134 | const arr = collectDeepNodes(node, ts.SyntaxKind.ArrayLiteralExpression) 135 | 136 | if(!arr || arr.length === 0 || arr[0].elements.length === 0) { 137 | return 138 | } 139 | 140 | return { 141 | type: 'style', 142 | path: arr[0].elements.map(element => getResourceRequest(element, sourceFile)), 143 | node 144 | } 145 | } 146 | }) 147 | .filter(resource => resource !== undefined) as SourceFileResource[] 148 | } 149 | 150 | function getContentOfKeyLiteral(node?: ts.Node): string | null { 151 | if(!node) { 152 | return null 153 | } 154 | else if(node.kind === ts.SyntaxKind.Identifier) { 155 | return (node as ts.Identifier).text 156 | } 157 | else if(node.kind === ts.SyntaxKind.StringLiteral) { 158 | return (node as ts.StringLiteral).text 159 | } 160 | else { 161 | return null 162 | } 163 | } 164 | 165 | function getResourceRequest(element: ts.Expression, sourceFile: ts.SourceFile) { 166 | let path: string|null = null 167 | 168 | if( 169 | element.kind === ts.SyntaxKind.StringLiteral || 170 | element.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral 171 | ) { 172 | const url = (element as ts.StringLiteral).text 173 | 174 | // If the URL does not start with ./ or ../, prepends ./ to it. 175 | path = `${/^\.?\.\//.test(url) ? '' : './'}${url}` 176 | } else { 177 | // if not string, just use expression directly 178 | path = element.getFullText(sourceFile) 179 | } 180 | 181 | const directory = dirname(sourceFile.fileName) 182 | 183 | return resolve(directory, path) 184 | } 185 | 186 | function createPreProcessorNode(path: string, type: 'template'|'style') { 187 | const base64 = new Buffer(path).toString('base64') 188 | 189 | return ts.createLiteral(`_PRAGMA_PARCEL_TYPESCRIPT_PLUGIN_PREPROCESS_${type.toUpperCase()}(${base64})`) 190 | } 191 | --------------------------------------------------------------------------------