From dc6940de2e42efdeb0c1fd2d58c03283ab367ca4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 18 Jun 2015 20:21:00 -0400 Subject: [PATCH 1/9] apply resolvePath hook to entry module (#30) --- src/Bundle.js | 16 ++++----- src/Module.js | 3 +- src/utils/resolvePath.js | 3 ++ .../custom-path-resolver-on-entry/_config.js | 35 +++++++++++++++++++ .../custom-path-resolver-on-entry/bar.js | 3 ++ .../custom-path-resolver-on-entry/foo.js | 5 +++ 6 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 test/function/custom-path-resolver-on-entry/_config.js create mode 100644 test/function/custom-path-resolver-on-entry/bar.js create mode 100644 test/function/custom-path-resolver-on-entry/foo.js diff --git a/src/Bundle.js b/src/Bundle.js index 975314f..494465e 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -1,4 +1,4 @@ -import { basename, dirname, extname, relative, resolve } from 'path'; +import { basename, dirname, extname, relative } from 'path'; import { Promise } from 'sander'; import MagicString from 'magic-string'; import { blank, keys } from './utils/object'; @@ -15,9 +15,11 @@ import { unixizePath } from './utils/normalizePlatform.js'; export default class Bundle { constructor ( options ) { - this.entryPath = resolve( options.entry ).replace( /\.js$/, '' ) + '.js'; - this.base = dirname( this.entryPath ); + this.entry = options.entry; + this.entryModule = null; + // TODO resolvePath is incorrect - it may not be a filesystem path, but + // something more abstract this.resolvePath = options.resolvePath || defaultResolver; this.load = options.load || defaultLoader; @@ -30,8 +32,6 @@ export default class Bundle { transform: ensureArray( options.transform ) }; - this.entryModule = null; - this.varExports = blank(); this.toExport = null; @@ -43,7 +43,7 @@ export default class Bundle { } fetchModule ( importee, importer ) { - return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer, this.resolvePathOptions ) ) + return Promise.resolve( this.resolvePath( importee, importer, this.resolvePathOptions ) ) .then( path => { if ( !path ) { // external module @@ -75,7 +75,7 @@ export default class Bundle { build () { // bring in top-level AST nodes from the entry module - return this.fetchModule( this.entryPath, null ) + return this.fetchModule( this.entry, undefined ) .then( entryModule => { const defaultExport = entryModule.exports.default; @@ -91,7 +91,7 @@ export default class Bundle { // `export default a + b` - generate an export name // based on the filename of the entry module else { - let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) ); + let defaultExportName = makeLegalIdentifier( basename( this.entryModule.path ).slice( 0, -extname( this.entryModule.path ).length ) ); // deconflict let topLevelNames = []; diff --git a/src/Module.js b/src/Module.js index c0900c4..bb2adce 100644 --- a/src/Module.js +++ b/src/Module.js @@ -1,3 +1,4 @@ +import { dirname } from 'path'; import { Promise } from 'sander'; import { parse } from 'acorn'; import MagicString from 'magic-string'; @@ -251,7 +252,7 @@ export default class Module { getCanonicalName ( localName ) { // Special case if ( localName === 'default' && this.exports.default && this.exports.default.isModified ) { - let canonicalName = makeLegalIdentifier( this.path.replace( this.bundle.base + '/', '' ).replace( /\.js$/, '' ) ); + let canonicalName = makeLegalIdentifier( this.path.replace( dirname( this.bundle.entryModule.path ) + '/', '' ).replace( /\.js$/, '' ) ); while ( this.definitions[ canonicalName ] ) { canonicalName = `_${canonicalName}`; } diff --git a/src/utils/resolvePath.js b/src/utils/resolvePath.js index 77fe421..5f9de68 100644 --- a/src/utils/resolvePath.js +++ b/src/utils/resolvePath.js @@ -7,6 +7,9 @@ export function defaultResolver ( importee, importer, options ) { // absolute paths are left untouched if ( absolutePath.test( importee ) ) return importee; + // if this is the entry point, resolve against cwd + if ( importer === undefined ) return resolve( importee ); + // we try to resolve external modules if ( importee[0] !== '.' ) { // unless we want to keep it external, that is diff --git a/test/function/custom-path-resolver-on-entry/_config.js b/test/function/custom-path-resolver-on-entry/_config.js new file mode 100644 index 0000000..4d22164 --- /dev/null +++ b/test/function/custom-path-resolver-on-entry/_config.js @@ -0,0 +1,35 @@ +var path = require( 'path' ); +var fs = require( 'fs' ); +var assert = require( 'assert' ); + +var cachedModules = { + '@main.js': 'import foo from "./foo"; export default foo();' +}; + +module.exports = { + description: 'applies custom resolver to entry point', + //solo: true, + options: { + resolvePath: function ( importee, importer ) { + if ( importer === undefined ) { + return '@' + path.relative( __dirname, importee ); + } + + if ( importer[0] === '@' ) { + return path.resolve( __dirname, importee ) + '.js'; + } + + return path.resolve( path.dirname( importer ), importee ) + '.js'; + }, + load: function ( moduleId ) { + if ( moduleId[0] === '@' ) { + return cachedModules[ moduleId ]; + } + + return fs.readFileSync( moduleId, 'utf-8' ); + } + }, + exports: function ( exports ) { + assert.equal( exports, 42 ); + } +}; diff --git a/test/function/custom-path-resolver-on-entry/bar.js b/test/function/custom-path-resolver-on-entry/bar.js new file mode 100644 index 0000000..562d38d --- /dev/null +++ b/test/function/custom-path-resolver-on-entry/bar.js @@ -0,0 +1,3 @@ +export default function () { + return 21; +} diff --git a/test/function/custom-path-resolver-on-entry/foo.js b/test/function/custom-path-resolver-on-entry/foo.js new file mode 100644 index 0000000..642dadd --- /dev/null +++ b/test/function/custom-path-resolver-on-entry/foo.js @@ -0,0 +1,5 @@ +import bar from './bar'; + +export default function () { + return bar() * 2; +} From 5cd2208714ef833e5cab7f5dd69c520c21cdf947 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 18 Jun 2015 20:21:08 -0400 Subject: [PATCH 2/9] update tests --- test/function/custom-path-resolver-async/_config.js | 5 ++++- test/function/custom-path-resolver-sync/_config.js | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/function/custom-path-resolver-async/_config.js b/test/function/custom-path-resolver-async/_config.js index acb62d0..82ed21a 100644 --- a/test/function/custom-path-resolver-async/_config.js +++ b/test/function/custom-path-resolver-async/_config.js @@ -1,3 +1,4 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { @@ -7,8 +8,10 @@ module.exports = { var Promise = require( 'sander' ).Promise; var resolved; + if ( importee === path.resolve( __dirname, 'main.js' ) ) return importee; + if ( importee === 'foo' ) { - resolved = require( 'path' ).resolve( __dirname, 'bar.js' ); + resolved = path.resolve( __dirname, 'bar.js' ); } else { resolved = false; } diff --git a/test/function/custom-path-resolver-sync/_config.js b/test/function/custom-path-resolver-sync/_config.js index a7cd96b..4df8fcf 100644 --- a/test/function/custom-path-resolver-sync/_config.js +++ b/test/function/custom-path-resolver-sync/_config.js @@ -1,12 +1,12 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { description: 'uses a custom path resolver (synchronous)', options: { resolvePath: function ( importee, importer ) { - if ( importee === 'foo' ) { - return require( 'path' ).resolve( __dirname, 'bar.js' ); - } + if ( importee === path.resolve( __dirname, 'main.js' ) ) return importee; + if ( importee === 'foo' ) return path.resolve( __dirname, 'bar.js' ); return false; } From b8894dd9014c2a6d54a66673f321b3acfb9821d2 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 28 Jun 2015 17:53:55 -0400 Subject: [PATCH 3/9] rename things internally (path -> id, etc) - #30 --- src/Bundle.js | 26 +++++++++---------- src/Module.js | 20 +++++++------- src/Statement.js | 2 +- src/utils/load.js | 6 ++--- src/utils/{resolvePath.js => resolveId.js} | 0 .../custom-path-resolver-async/_config.js | 2 +- .../custom-path-resolver-on-entry/_config.js | 2 +- .../custom-path-resolver-sync/_config.js | 2 +- 8 files changed, 30 insertions(+), 30 deletions(-) rename src/utils/{resolvePath.js => resolveId.js} (100%) diff --git a/src/Bundle.js b/src/Bundle.js index 494465e..90a21c6 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -7,7 +7,7 @@ import ExternalModule from './ExternalModule'; import finalisers from './finalisers/index'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; import ensureArray from './utils/ensureArray'; -import { defaultResolver, defaultExternalResolver } from './utils/resolvePath'; +import { defaultResolver, defaultExternalResolver } from './utils/resolveId'; import { defaultLoader } from './utils/load'; import getExportMode from './utils/getExportMode'; import getIndentString from './utils/getIndentString'; @@ -18,12 +18,10 @@ export default class Bundle { this.entry = options.entry; this.entryModule = null; - // TODO resolvePath is incorrect - it may not be a filesystem path, but - // something more abstract - this.resolvePath = options.resolvePath || defaultResolver; + this.resolveId = options.resolveId || defaultResolver; this.load = options.load || defaultLoader; - this.resolvePathOptions = { + this.resolveOptions = { external: ensureArray( options.external ), resolveExternal: options.resolveExternal || defaultExternalResolver }; @@ -43,9 +41,9 @@ export default class Bundle { } fetchModule ( importee, importer ) { - return Promise.resolve( this.resolvePath( importee, importer, this.resolvePathOptions ) ) - .then( path => { - if ( !path ) { + return Promise.resolve( this.resolveId( importee, importer, this.resolveOptions ) ) + .then( id => { + if ( !id ) { // external module if ( !this.modulePromises[ importee ] ) { const module = new ExternalModule( importee ); @@ -56,11 +54,11 @@ export default class Bundle { return this.modulePromises[ importee ]; } - if ( !this.modulePromises[ path ] ) { - this.modulePromises[ path ] = Promise.resolve( this.load( path, this.loadOptions ) ) + if ( !this.modulePromises[ id ] ) { + this.modulePromises[ id ] = Promise.resolve( this.load( id, this.loadOptions ) ) .then( source => { const module = new Module({ - path, + id, source, bundle: this }); @@ -69,7 +67,7 @@ export default class Bundle { }); } - return this.modulePromises[ path ]; + return this.modulePromises[ id ]; }); } @@ -89,9 +87,9 @@ export default class Bundle { } // `export default a + b` - generate an export name - // based on the filename of the entry module + // based on the id of the entry module else { - let defaultExportName = makeLegalIdentifier( basename( this.entryModule.path ).slice( 0, -extname( this.entryModule.path ).length ) ); + let defaultExportName = makeLegalIdentifier( basename( this.entryModule.id ).slice( 0, -extname( this.entryModule.id ).length ) ); // deconflict let topLevelNames = []; diff --git a/src/Module.js b/src/Module.js index 96f20fd..4297fa4 100644 --- a/src/Module.js +++ b/src/Module.js @@ -22,14 +22,16 @@ function deconflict ( name, names ) { } export default class Module { - constructor ({ path, source, bundle }) { + constructor ({ id, source, bundle }) { this.source = source; this.bundle = bundle; - this.path = path; + this.id = id; + // By default, `id` is the filename. Custom resolvers and loaders + // can change that, but it makes sense to use it for the source filename this.magicString = new MagicString( source, { - filename: path + filename: id }); this.suggestedNames = blank(); @@ -47,7 +49,7 @@ export default class Module { }); } catch ( err ) { err.code = 'PARSE_ERROR'; - err.file = path; + err.file = id; // see above - not necessarily true, but true enough throw err; } @@ -114,7 +116,7 @@ export default class Module { if ( this.imports[ localName ] ) { const err = new Error( `Duplicated import '${localName}'` ); - err.file = this.path; + err.file = this.id; err.loc = getLocation( this.source, specifier.start ); throw err; } @@ -260,7 +262,7 @@ export default class Module { getCanonicalName ( localName ) { // Special case if ( localName === 'default' && ( this.exports.default.isModified || !this.suggestedNames.default ) ) { - let canonicalName = makeLegalIdentifier( this.path.replace( dirname( this.bundle.entryModule.path ) + '/', '' ).replace( /\.js$/, '' ) ); + let canonicalName = makeLegalIdentifier( this.id.replace( dirname( this.bundle.entryModule.id ) + '/', '' ).replace( /\.js$/, '' ) ); return deconflict( canonicalName, this.definitions ); } @@ -313,7 +315,7 @@ export default class Module { if ( this.imports[ name ] ) { const importDeclaration = this.imports[ name ]; - promise = this.bundle.fetchModule( importDeclaration.source, this.path ) + promise = this.bundle.fetchModule( importDeclaration.source, this.id ) .then( module => { importDeclaration.module = module; @@ -359,7 +361,7 @@ export default class Module { const exportDeclaration = module.exports[ importDeclaration.name ]; if ( !exportDeclaration ) { - throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` ); + throw new Error( `Module ${module.id} does not export ${importDeclaration.name} (imported by ${this.id})` ); } return module.define( exportDeclaration.localName ); @@ -434,7 +436,7 @@ export default class Module { // ...unless they're empty, in which case assume we're importing them for the side-effects // THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar if ( !statement.node.specifiers.length ) { - return this.bundle.fetchModule( statement.node.source.value, this.path ) + return this.bundle.fetchModule( statement.node.source.value, this.id ) .then( module => { statement.module = module; return module.expandAllStatements(); diff --git a/src/Statement.js b/src/Statement.js index 9a1933f..4dbe619 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -164,7 +164,7 @@ export default class Statement { if ( depth < minDepth ) { const err = new Error( `Illegal reassignment to import '${node.name}'` ); - err.file = this.module.path; + err.file = this.module.id; err.loc = getLocation( this.module.magicString.toString(), node.start ); throw err; } diff --git a/src/utils/load.js b/src/utils/load.js index 9b79d9a..60b7566 100644 --- a/src/utils/load.js +++ b/src/utils/load.js @@ -1,10 +1,10 @@ import { readFileSync } from 'sander'; -export function defaultLoader ( path, options ) { +export function defaultLoader ( id, options ) { // TODO support plugins e.g. !css and !json? - const source = readFileSync( path, { encoding: 'utf-8' }); + const source = readFileSync( id, { encoding: 'utf-8' }); return options.transform.reduce( ( source, transformer ) => { - return transformer( source, path ); + return transformer( source, id ); }, source ); } diff --git a/src/utils/resolvePath.js b/src/utils/resolveId.js similarity index 100% rename from src/utils/resolvePath.js rename to src/utils/resolveId.js diff --git a/test/function/custom-path-resolver-async/_config.js b/test/function/custom-path-resolver-async/_config.js index 82ed21a..287f536 100644 --- a/test/function/custom-path-resolver-async/_config.js +++ b/test/function/custom-path-resolver-async/_config.js @@ -4,7 +4,7 @@ var assert = require( 'assert' ); module.exports = { description: 'uses a custom path resolver (asynchronous)', options: { - resolvePath: function ( importee, importer ) { + resolveId: function ( importee, importer ) { var Promise = require( 'sander' ).Promise; var resolved; diff --git a/test/function/custom-path-resolver-on-entry/_config.js b/test/function/custom-path-resolver-on-entry/_config.js index 4d22164..83a6a67 100644 --- a/test/function/custom-path-resolver-on-entry/_config.js +++ b/test/function/custom-path-resolver-on-entry/_config.js @@ -10,7 +10,7 @@ module.exports = { description: 'applies custom resolver to entry point', //solo: true, options: { - resolvePath: function ( importee, importer ) { + resolveId: function ( importee, importer ) { if ( importer === undefined ) { return '@' + path.relative( __dirname, importee ); } diff --git a/test/function/custom-path-resolver-sync/_config.js b/test/function/custom-path-resolver-sync/_config.js index 4df8fcf..9a755e5 100644 --- a/test/function/custom-path-resolver-sync/_config.js +++ b/test/function/custom-path-resolver-sync/_config.js @@ -4,7 +4,7 @@ var assert = require( 'assert' ); module.exports = { description: 'uses a custom path resolver (synchronous)', options: { - resolvePath: function ( importee, importer ) { + resolveId: function ( importee, importer ) { if ( importee === path.resolve( __dirname, 'main.js' ) ) return importee; if ( importee === 'foo' ) return path.resolve( __dirname, 'bar.js' ); From 7eeaf73eb1b67a568db88788524e87b8acff6f5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 8 Jul 2015 18:38:42 -0400 Subject: [PATCH 4/9] failing test for #36 --- test/function/cycles-pathological/A.js | 11 +++++++++++ test/function/cycles-pathological/B.js | 8 ++++++++ test/function/cycles-pathological/C.js | 8 ++++++++ test/function/cycles-pathological/D.js | 11 +++++++++++ test/function/cycles-pathological/_config.js | 19 +++++++++++++++++++ test/function/cycles-pathological/main.js | 12 ++++++++++++ 6 files changed, 69 insertions(+) create mode 100644 test/function/cycles-pathological/A.js create mode 100644 test/function/cycles-pathological/B.js create mode 100644 test/function/cycles-pathological/C.js create mode 100644 test/function/cycles-pathological/D.js create mode 100644 test/function/cycles-pathological/_config.js create mode 100644 test/function/cycles-pathological/main.js diff --git a/test/function/cycles-pathological/A.js b/test/function/cycles-pathological/A.js new file mode 100644 index 0000000..9e0fd2e --- /dev/null +++ b/test/function/cycles-pathological/A.js @@ -0,0 +1,11 @@ +import B from './B'; + +export default class A { + constructor () { + this.isA = true; + } + + b () { + return new B(); + } +} diff --git a/test/function/cycles-pathological/B.js b/test/function/cycles-pathological/B.js new file mode 100644 index 0000000..1c8e9b4 --- /dev/null +++ b/test/function/cycles-pathological/B.js @@ -0,0 +1,8 @@ +import A from './A'; + +export default class B extends A { + constructor () { + super(); + this.isB = true; + } +} diff --git a/test/function/cycles-pathological/C.js b/test/function/cycles-pathological/C.js new file mode 100644 index 0000000..313bba7 --- /dev/null +++ b/test/function/cycles-pathological/C.js @@ -0,0 +1,8 @@ +import D from './D'; + +export default class C extends D { + constructor () { + super(); + this.isC = true; + } +} diff --git a/test/function/cycles-pathological/D.js b/test/function/cycles-pathological/D.js new file mode 100644 index 0000000..2d8a405 --- /dev/null +++ b/test/function/cycles-pathological/D.js @@ -0,0 +1,11 @@ +import C from './C'; + +export default class D { + constructor () { + this.isD = true; + } + + c () { + return new C(); + } +} diff --git a/test/function/cycles-pathological/_config.js b/test/function/cycles-pathological/_config.js new file mode 100644 index 0000000..801659f --- /dev/null +++ b/test/function/cycles-pathological/_config.js @@ -0,0 +1,19 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'resolves pathological cyclical dependencies gracefully', + babel: true, + exports: function ( exports ) { + assert.ok( exports.a.isA ); + assert.ok( exports.b1.isA ); + assert.ok( exports.b1.isB ); + assert.ok( exports.b2.isA ); + assert.ok( exports.b2.isB ); + assert.ok( exports.c1.isC ); + assert.ok( exports.c1.isD ); + assert.ok( exports.c2.isC ); + assert.ok( exports.c2.isD ); + assert.ok( exports.d.isD ); + }, + solo: true +}; diff --git a/test/function/cycles-pathological/main.js b/test/function/cycles-pathological/main.js new file mode 100644 index 0000000..f4c19db --- /dev/null +++ b/test/function/cycles-pathological/main.js @@ -0,0 +1,12 @@ +import A from './A'; +import B from './B'; + +import C from './C'; +import D from './D'; + +export const a = new A(); +export const b1 = a.b(); +export const b2 = new B(); +export const c1 = new C(); +export const d = new D(); +export const c2 = d.c(); From 5a548049bf4f6d8b7f5e6b56ee2c6fae9b336dc7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2015 17:02:59 -0400 Subject: [PATCH 5/9] remove unused check --- src/Statement.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Statement.js b/src/Statement.js index 9a1933f..358c703 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -206,8 +206,7 @@ export default class Statement { } expand () { - if ( this.isIncluded ) return emptyArrayPromise; // TODO can this happen? - this.isIncluded = true; + this.isIncluded = true; // prevent statement being included twice let result = []; From b49b35fbb0cd1229eb50a1a35cb52b870eaa68a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2015 17:03:13 -0400 Subject: [PATCH 6/9] mark strong dependencies --- src/Statement.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Statement.js b/src/Statement.js index 358c703..c15f896 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -17,6 +17,7 @@ export default class Statement { this.defines = blank(); this.modifies = blank(); this.dependsOn = blank(); + this.stronglyDependsOn = blank(); this.isIncluded = false; @@ -140,6 +141,8 @@ export default class Statement { if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) { this.dependsOn[ node.name ] = true; + + if ( !scope.parent ) this.stronglyDependsOn[ node.name ] = true; } } } From 67d9b5847e0acdf68eda9450aa99e70acefc176a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2015 17:03:45 -0400 Subject: [PATCH 7/9] prevent class method definitions erroneously appearing in statement.dependsOn --- src/Statement.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Statement.js b/src/Statement.js index c15f896..7bde15e 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -137,6 +137,9 @@ export default class Statement { return; } + // disregard the `bar` in `class Foo { bar () {...} }` + if ( parent.type === 'MethodDefinition' ) return; + const definingScope = scope.findDefiningScope( node.name ); if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) { From aacbce87595f681689ca4b8198534e5570b5b991 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2015 17:05:18 -0400 Subject: [PATCH 8/9] prevent some unnecessary work --- src/Statement.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Statement.js b/src/Statement.js index 7bde15e..0d665f6 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -221,6 +221,8 @@ export default class Statement { const dependencies = Object.keys( this.dependsOn ); return sequence( dependencies, name => { + if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place? + return this.module.define( name ).then( definition => { result.push.apply( result, definition ); }); From b7e51064ac4d28c05a13adaa5387b163ab881b1c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2015 18:10:57 -0400 Subject: [PATCH 9/9] borrow sorting logic from esperanto - fixes #36 --- src/Bundle.js | 71 ++++++++++++++++++++ src/Statement.js | 3 +- test/function/cycles-pathological/_config.js | 3 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/Bundle.js b/src/Bundle.js index 975314f..baebff2 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -112,6 +112,7 @@ export default class Bundle { .then( statements => { this.statements = statements; this.deconflict(); + this.sort(); }); } @@ -200,6 +201,76 @@ export default class Bundle { } } + sort () { + // TODO avoid this work whenever possible... + + let definitions = blank(); + + // gather definitions + this.statements.forEach( statement => { + keys( statement.defines ).forEach( name => { + const canonicalName = statement.module.getCanonicalName( name ); + definitions[ canonicalName ] = statement; + }); + }); + + let strongDeps = blank(); + let stronglyDependsOn = blank(); + + this.statements.forEach( statement => { + const id = statement.id; + strongDeps[ id ] = []; + stronglyDependsOn[ id ] = {}; + + keys( statement.stronglyDependsOn ).forEach( name => { + if ( statement.defines[ name ] ) return; // TODO seriously... need to fix this + const canonicalName = statement.module.getCanonicalName( name ); + const definition = definitions[ canonicalName ]; + + if ( definition ) strongDeps[ statement.id ].push( definition ); + }); + }); + + // add second (and third...) order strong dependencies + this.statements.forEach( statement => { + const id = statement.id; + + // add second (and third...) order dependencies + function addStrongDependencies ( dependency ) { + if ( stronglyDependsOn[ id ][ dependency.id ] ) return; + + stronglyDependsOn[ id ][ dependency.id ] = true; + strongDeps[ dependency.id ].forEach( addStrongDependencies ); + } + + strongDeps[ id ].forEach( addStrongDependencies ); + }); + + // reinsert each statement, ensuring its strong dependencies appear first + let sorted = []; + let included = blank(); + + this.statements.forEach( statement => { + strongDeps[ statement.id ].forEach( place ); + + function place ( dependency ) { + if ( !stronglyDependsOn[ dependency.id ][ statement.id ] && !included[ dependency.id ] ) { + strongDeps[ dependency.id ].forEach( place ); + sorted.push( dependency ); + + included[ dependency.id ] = true; + } + } + + if ( !included[ statement.id ] ) { + sorted.push( statement ); + included[ statement.id ] = true; + } + }); + + this.statements = sorted; + } + generate ( options = {} ) { let magicString = new MagicString.Bundle({ separator: '' }); diff --git a/src/Statement.js b/src/Statement.js index 0d665f6..15a9f5c 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -4,14 +4,13 @@ import getLocation from './utils/getLocation'; import walk from './ast/walk'; import Scope from './ast/Scope'; -const emptyArrayPromise = Promise.resolve([]); - export default class Statement { constructor ( node, magicString, module, index ) { this.node = node; this.module = module; this.magicString = magicString; this.index = index; + this.id = module.path + '#' + index; this.scope = new Scope(); this.defines = blank(); diff --git a/test/function/cycles-pathological/_config.js b/test/function/cycles-pathological/_config.js index 801659f..156a2f3 100644 --- a/test/function/cycles-pathological/_config.js +++ b/test/function/cycles-pathological/_config.js @@ -14,6 +14,5 @@ module.exports = { assert.ok( exports.c2.isC ); assert.ok( exports.c2.isD ); assert.ok( exports.d.isD ); - }, - solo: true + } };