diff --git a/README.md b/README.md index b463f88..6341cfd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,122 @@ -wro4j-runner -============ +# wro4j-runner + Command line runner for wro4j -Building -============ +# Building +``` + $ git clone git@github.com:wro4j/wro4j-runner.git + $ mvn clean install +``` + +The compiled file will be saved at: + target/wro4j-runner-1.7.6.jar + +# How to Use wro4j-runner + +To use `wro4j-runner`, you need to provide a configuration file (`wro.xml`), a properties file (`wro.properties`), and optionally a `.jshintrc` file for JavaScript linting. + ``` -$ git clone git@github.com:wro4j/wro4j-runner.git -$ mvn clean install + ======================================= + USAGE + ======================================= + --contextFolder PATH : Folder used as a root of the context relative + resources. By default this is the user current + folder. + --destinationFolder PATH : Where to store the processed result. By default + uses the folder named [wro]. + --parallel : Turns on the parallel preProcessing of resources. + This value is false by default. + --postProcessors POST_PROCESSOR : Comma separated list of post-processors + --targetGroups GROUPS : Comma separated value of the group names from + wro.xml to process. If none is provided, all + groups will be processed. + --wroConfigurationFile PATH_TO_WRO_PROPERTIES : The path to the wro.properties file. By default + the configuration file is searched inse the user + current folder. + --wroFile PATH_TO_WRO_XML : The path to the wro model file. By default the + model is searched inse the user current folder. + -c (--compressor, --preProcessors) COMPRESSOR : Comma separated list of pre-processors + -i (--ignoreMissingResources) : Ignores missing resources + -m (--minimize) : Turns on the minimization by applying compressor ``` + +### 1. Create `wro.xml` + +This XML file defines your resource groups and the resources (JS/CSS files) to process. Example: + +```xml + + + /js/script1.js + /js/script2.js + /js/**.js + /css/style1.css + + +``` + +For a complete explanation on how to use wro.xml please visit: +https://wro4j.readthedocs.io/en/stable/WroFileFormat/ + +### 2. Create `wro.properties` + +This file configures wro4j options, such as pre-processors and output directories. Example: + +```properties +preProcessors=cssUrlRewriting,cssMinJawr,semicolonAppender,jsMin +targetGroups=all +destinationFolder=dist +``` + +For a complete list of available Configuration options please visit: +https://wro4j.readthedocs.io/en/stable/ConfigurationOptions/ + +### 3. Create `.jshintrc` (Optional) + +If you want to enable JS linting, add a `.jshintrc` file with your linting rules: + +```json +{ + "esnext": true, + "strict": true, + "undef": true, + "unused": true, + "eqeqeq": true, + "curly": true, + "browser": true, + "node": true, + "devel": true, + "asi": false, + "maxerr": 50, + "latedef": true, + "noarg": true, + "nonew": true, + "camelcase": true, + "quotmark": true, + "trailing": true, + "freeze": true, + "futurehostile": true, + "nocomma": true, + "varstmt": true +} +``` + +For a complete list of available options please visit: +https://jshint.com/docs/options/ + +### 4. Run wro4j-runner + +Run the tool from the command line, specifying the configuration files: + +```sh +java -jar ../wro4j-runner-1.7.6.jar \ + --wroFile wro.xml \ + --contextFolder test_code_dir \ + --postProcessors jsMin \ + --wroConfigurationFile wro.properties \ + --destinationFolder test_jshint_reports \ + --minimize +``` + +Adjust the paths as needed for your project structure. diff --git a/pom.xml b/pom.xml index 7234f9a..9838dc7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,14 +7,19 @@ 1.7.5 wro4j-runner - 1.7.6 + 1.7.8 jar wro4j runner - 1.7.7 + 1.8.2 ro.isdc.wro.runner.Wro4jCommandLineRunner + + ${project.groupId} + wro4j-core + ${wro4j.version} + ${project.groupId} wro4j-extensions @@ -29,7 +34,7 @@ com.google.code.gson gson - provided + compile org.jruby @@ -83,6 +88,12 @@ slf4j-log4j12 compile + + org.codehaus.groovy + groovy-all + 2.1.6 + compile + diff --git a/src/main/java/ro/isdc/wro/runner/Wro4jCommandLineRunner.java b/src/main/java/ro/isdc/wro/runner/Wro4jCommandLineRunner.java index 177330c..50c0991 100644 --- a/src/main/java/ro/isdc/wro/runner/Wro4jCommandLineRunner.java +++ b/src/main/java/ro/isdc/wro/runner/Wro4jCommandLineRunner.java @@ -58,7 +58,6 @@ import ro.isdc.wro.util.StopWatch; import ro.isdc.wro.util.io.UnclosableBufferedInputStream; - /** * Default command line runner. Interprets arguments and perform a processing. * @@ -66,362 +65,373 @@ * @since 1.3.4 */ public class Wro4jCommandLineRunner { - private static final Logger LOG = LoggerFactory.getLogger(Wro4jCommandLineRunner.class); - private static String userDirectory = System.getProperty("user.dir"); - private final File defaultWroFile = newDefaultWroFile(); - - @Option(name = "-m", aliases = { - "--minimize" - }, usage = "Turns on the minimization by applying compressor") - private boolean minimize; - @Option(name = "--parallel", usage = "Turns on the parallel preProcessing of resources. This value is false by default.") - private boolean parallelPreprocessing; - @Option(name = "--targetGroups", metaVar = "GROUPS", usage = "Comma separated value of the group names from wro.xml to process. If none is provided, all groups will be processed.") - private String targetGroups; - @Option(name = "-i", aliases = { - "--ignoreMissingResources" - }, usage = "Ignores missing resources") - private boolean ignoreMissingResources; - @Option(name = "--wroFile", metaVar = "PATH_TO_WRO_XML", usage = "The path to the wro model file. By default the model is searched inse the user current folder.") - private final File wroFile = defaultWroFile; - @Option(name = "--wroConfigurationFile", metaVar = "PATH_TO_WRO_PROPERTIES", usage = "The path to the wro.properties file. By default the configuration file is searched inse the user current folder.") - private final File wroConfigurationFile = newWroConfigurationFile(); - @Option(name = "--contextFolder", metaVar = "PATH", usage = "Folder used as a root of the context relative resources. By default this is the user current folder.") - private final File contextFolder = new File(System.getProperty("user.dir")); - @Option(name = "--destinationFolder", metaVar = "PATH", usage = "Where to store the processed result. By default uses the folder named [wro].") - private File destinationFolder = new File(System.getProperty("user.dir"), "wro"); - @Option(name = "-c", aliases = { - "--compressor", "--preProcessors" - }, metaVar = "COMPRESSOR", usage = "Comma separated list of pre-processors") - private String preProcessorsList; - @Option(name = "--postProcessors", metaVar = "POST_PROCESSOR", usage = "Comma separated list of post-processors") - private String postProcessorsList; - - private Properties wroConfigurationAsProperties; - - public static void main(final String[] args) - throws Exception { - new Wro4jCommandLineRunner().doMain(args); - } - - /** - * @return the location where wro file is located by default. Default implementation uses current user directory. - * @VisibleForTesting - */ - protected File newDefaultWroFile() { - return new File(userDirectory, "wro.xml"); - } - - /** - * @return the location where wro configuration file is located by default. Default implementation uses current user - * directory. - * @VisibleForTesting - */ - protected File newWroConfigurationFile() { - return new File(userDirectory, "wro.properties"); - } - - /** - * @return the context folder used by runner. - * @VisibleForTesting - */ - protected File getContextFolder() { - return contextFolder; - } - - /** - * @return the destination folder where the result will be written. - * @VisibleForTesting - */ - protected File getDestinationFolder() { - return contextFolder; - } - - /** - * @param args - */ - protected void doMain(final String[] args) { - LOG.debug("arguments: " + Arrays.toString(args)); - final CmdLineParser parser = new CmdLineParser(this); - parser.setUsageWidth(100); - final StopWatch watch = new StopWatch(); - watch.start("processing"); - try { - parser.parseArgument(args); - LOG.debug("Options: {}", this); - process(); - } catch (final Exception e) { - e.printStackTrace(); - System.err.println(e.getMessage() + "\n\n"); - System.err.println("======================================="); - System.err.println("USAGE"); - System.err.println("======================================="); - parser.printUsage(System.err); - onRunnerException(e); - } finally { - watch.stop(); - LOG.debug(watch.prettyPrint()); - LOG.info("Processing took: {}ms", watch.getLastTaskTimeMillis()); + private static final Logger LOG = LoggerFactory.getLogger(Wro4jCommandLineRunner.class); + private static String userDirectory = System.getProperty("user.dir"); + private final File defaultWroFile = newDefaultWroFile(); + + @Option(name = "-m", aliases = { + "--minimize" + }, usage = "Turns on the minimization by applying compressor") + private boolean minimize; + @Option(name = "--parallel", usage = "Turns on the parallel preProcessing of resources. This value is false by default.") + private boolean parallelPreprocessing; + @Option(name = "--targetGroups", metaVar = "GROUPS", usage = "Comma separated value of the group names from wro.xml to process. If none is provided, all groups will be processed.") + private String targetGroups; + @Option(name = "-i", aliases = { + "--ignoreMissingResources" + }, usage = "Ignores missing resources") + private boolean ignoreMissingResources; + @Option(name = "--wroFile", metaVar = "PATH_TO_WRO_XML", usage = "The path to the wro model file. By default the model is searched inse the user current folder.") + private final File wroFile = defaultWroFile; + @Option(name = "--wroConfigurationFile", metaVar = "PATH_TO_WRO_PROPERTIES", usage = "The path to the wro.properties file. By default the configuration file is searched inse the user current folder.") + private final File wroConfigurationFile = newWroConfigurationFile(); + @Option(name = "--contextFolder", metaVar = "PATH", usage = "Folder used as a root of the context relative resources. By default this is the user current folder.") + private final File contextFolder = new File(System.getProperty("user.dir")); + @Option(name = "--destinationFolder", metaVar = "PATH", usage = "Where to store the processed result. By default uses the folder named [wro].") + private File destinationFolder = new File(System.getProperty("user.dir"), "wro"); + @Option(name = "-c", aliases = { + "--compressor", "--preProcessors" + }, metaVar = "COMPRESSOR", usage = "Comma separated list of pre-processors") + private String preProcessorsList; + @Option(name = "--postProcessors", metaVar = "POST_PROCESSOR", usage = "Comma separated list of post-processors") + private String postProcessorsList; + + private Properties wroConfigurationAsProperties; + + public static void main(final String[] args) + throws Exception { + new Wro4jCommandLineRunner().doMain(args); + } + + /** + * @return the location where wro file is located by default. Default + * implementation uses current user directory. + * @VisibleForTesting + */ + protected File newDefaultWroFile() { + return new File(userDirectory, "wro.xml"); } - } - - /** - * Exception handler. - */ - protected void onRunnerException(final Exception e) { - System.out.println(e.getMessage()); - System.exit(1); // non-zero exit code indicates there was an error - } - - private void process() { - try { - Context.set(Context.standaloneContext()); - // create destinationFolder if needed - if (!destinationFolder.exists()) { - destinationFolder.mkdirs(); - } - final Collection groupsAsList = getTargetGroupsAsList(); - for (final String group : groupsAsList) { - for (final ResourceType resourceType : ResourceType.values()) { - final String groupWithExtension = group + "." + resourceType.name().toLowerCase(); - processGroup(groupWithExtension, destinationFolder); + + /** + * @return the location where wro configuration file is located by default. + * Default implementation uses current user + * directory. + * @VisibleForTesting + */ + protected File newWroConfigurationFile() { + return new File(userDirectory, "wro.properties"); + } + + /** + * @return the context folder used by runner. + * @VisibleForTesting + */ + protected File getContextFolder() { + return contextFolder; + } + + /** + * @return the destination folder where the result will be written. + * @VisibleForTesting + */ + protected File getDestinationFolder() { + return contextFolder; + } + + /** + * @param args + */ + protected void doMain(final String[] args) { + LOG.debug("arguments: " + Arrays.toString(args)); + final CmdLineParser parser = new CmdLineParser(this); + parser.setUsageWidth(100); + final StopWatch watch = new StopWatch(); + watch.start("processing"); + try { + parser.parseArgument(args); + LOG.debug("Options: {}", this); + process(); + } catch (final Exception e) { + e.printStackTrace(); + System.err.println(e.getMessage() + "\n\n"); + System.err.println("======================================="); + System.err.println("USAGE"); + System.err.println("======================================="); + parser.printUsage(System.err); + onRunnerException(e); + } finally { + watch.stop(); + LOG.debug(watch.prettyPrint()); + LOG.info("Processing took: {}ms", watch.getLastTaskTimeMillis()); + } + } + + /** + * Exception handler. + */ + protected void onRunnerException(final Exception e) { + System.out.println(e.getMessage()); + System.exit(1); // non-zero exit code indicates there was an error + } + + private void process() { + try { + Context.set(Context.standaloneContext()); + // create destinationFolder if needed + if (!destinationFolder.exists()) { + destinationFolder.mkdirs(); + } + final Collection groupsAsList = getTargetGroupsAsList(); + for (final String group : groupsAsList) { + for (final ResourceType resourceType : ResourceType.values()) { + final String groupWithExtension = group + "." + resourceType.name().toLowerCase(); + processGroup(groupWithExtension, destinationFolder); + } + } + } catch (final IOException e) { + System.err.println(e.getMessage()); + } + } + + /** + * @return a list containing all groups needs to be processed. + */ + private List getTargetGroupsAsList() + throws IOException { + if (targetGroups == null) { + final WroModel model = getManagerFactory().create().getModelFactory().create(); + return new WroModelInspector(model).getGroupNames(); + } + return Arrays.asList(targetGroups.split(",")); + } + + /** + * Process a single group. + * + * @throws IOException + * if any IO related exception occurs. + */ + private void processGroup(final String group, final File parentFoder) + throws IOException { + final ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream(); + InputStream resultInputStream = null; + try { + LOG.info("processing group: " + group); + initContext(group, resultOutputStream); + doProcess(); + + // encode version & write result to file + resultInputStream = new UnclosableBufferedInputStream(resultOutputStream.toByteArray()); + final File destinationFile = new File(parentFoder, rename(group, resultInputStream)); + destinationFile.createNewFile(); + // allow the same stream to be read again + resultInputStream.reset(); + LOG.debug("Created file: {}", destinationFile.getName()); + + final OutputStream fos = new FileOutputStream(destinationFile); + // use reader to detect encoding + IOUtils.copy(resultInputStream, fos); + fos.close(); + // delete empty files + if (destinationFile.length() == 0) { + LOG.debug("No content found for group: {}", group); + destinationFile.delete(); + } else { + LOG.info("file size: {} -> {}bytes", destinationFile.getName(), destinationFile.length()); + LOG.info("{} ({}bytes) has been created!", destinationFile.getAbsolutePath(), destinationFile.length()); + } + } finally { + if (resultOutputStream != null) { + resultOutputStream.close(); + } + if (resultInputStream != null) { + resultInputStream.close(); + } } - } - } catch (final IOException e) { - System.err.println(e.getMessage()); } - } - - /** - * @return a list containing all groups needs to be processed. - */ - private List getTargetGroupsAsList() - throws IOException { - if (targetGroups == null) { - final WroModel model = getManagerFactory().create().getModelFactory().create(); - return new WroModelInspector(model).getGroupNames(); + + /** + * Initialize the context for standalone execution. + */ + private void initContext(final String group, final ByteArrayOutputStream resultOutputStream) + throws IOException { + // mock request + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getRequestURI()).thenReturn(group); + // mock response + final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(resultOutputStream)); + + // init context + Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), initWroConfiguration()); + Context.get().setAggregatedFolderPath(computeAggregatedFolderPath()); + } + + /** + * Perform actual processing by delegating the process call to + * {@link WroManager}. + * + * @throws IOException + * @VisibleForTesting + */ + void doProcess() + throws IOException { + // perform processing + getManagerFactory().create().process(); } - return Arrays.asList(targetGroups.split(",")); - } - - /** - * Process a single group. - * - * @throws IOException - * if any IO related exception occurs. - */ - private void processGroup(final String group, final File parentFoder) - throws IOException { - final ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream(); - InputStream resultInputStream = null; - try { - LOG.info("processing group: " + group); - initContext(group, resultOutputStream); - doProcess(); - - // encode version & write result to file - resultInputStream = new UnclosableBufferedInputStream(resultOutputStream.toByteArray()); - final File destinationFile = new File(parentFoder, rename(group, resultInputStream)); - destinationFile.createNewFile(); - // allow the same stream to be read again - resultInputStream.reset(); - LOG.debug("Created file: {}", destinationFile.getName()); - - final OutputStream fos = new FileOutputStream(destinationFile); - // use reader to detect encoding - IOUtils.copy(resultInputStream, fos); - fos.close(); - // delete empty files - if (destinationFile.length() == 0) { - LOG.debug("No content found for group: {}", group); - destinationFile.delete(); - } else { - LOG.info("file size: {} -> {}bytes", destinationFile.getName(), destinationFile.length()); - LOG.info("{} ({}bytes) has been created!", destinationFile.getAbsolutePath(), destinationFile.length()); - } - } finally { - if (resultOutputStream != null) { - resultOutputStream.close(); - } - if (resultInputStream != null) { - resultInputStream.close(); - } + + private WroConfiguration initWroConfiguration() + throws IOException { + final PropertyWroConfigurationFactory factory = new PropertyWroConfigurationFactory( + getWroConfigurationProperties()); + final WroConfiguration config = factory.create(); + // keep backward compatibility configuration of some config properties + config.setParallelPreprocessing(parallelPreprocessing); + return config; } - } - - /** - * Initialize the context for standalone execution. - */ - private void initContext(final String group, final ByteArrayOutputStream resultOutputStream) - throws IOException { - // mock request - final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getRequestURI()).thenReturn(group); - // mock response - final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); - Mockito.when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(resultOutputStream)); - - // init context - Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), initWroConfiguration()); - Context.get().setAggregatedFolderPath(computeAggregatedFolderPath()); - } - - /** - * Perform actual processing by delegating the process call to {@link WroManager}. - * - * @throws IOException - * @VisibleForTesting - */ - void doProcess() - throws IOException { - // perform processing - getManagerFactory().create().process(); - } - - private WroConfiguration initWroConfiguration() - throws IOException { - final PropertyWroConfigurationFactory factory = new PropertyWroConfigurationFactory(getWroConfigurationProperties()); - final WroConfiguration config = factory.create(); - // keep backward compatibility configuration of some config properties - config.setParallelPreprocessing(parallelPreprocessing); - return config; - } - - /** - * @return {@link Properties} object loaded from wro.properties file located in current user directory. This location - * is configurable. If the wro.properties file does not exist, the returned value will be an empty - * {@link Properties} object, equivalent to no config properties. - * @throws IOException - * if loaded file is corrupt. - */ - private Properties getWroConfigurationProperties() - throws IOException { - if (wroConfigurationAsProperties == null) { - try { - wroConfigurationAsProperties = new Properties(); - if (wroConfigurationFile != null && wroConfigurationFile.exists()) { - LOG.debug("Using {} to load WroConfiguration.", wroConfigurationFile.getPath()); - wroConfigurationAsProperties.load(new FileInputStream(wroConfigurationFile)); - } else { - LOG.warn("Configuration file: '{}' does not exist. Using default configuration.", wroConfigurationFile); + + /** + * @return {@link Properties} object loaded from wro.properties file located in + * current user directory. This location + * is configurable. If the wro.properties file does not exist, the + * returned value will be an empty + * {@link Properties} object, equivalent to no config properties. + * @throws IOException + * if loaded file is corrupt. + */ + private Properties getWroConfigurationProperties() + throws IOException { + if (wroConfigurationAsProperties == null) { + try { + wroConfigurationAsProperties = new Properties(); + if (wroConfigurationFile != null && wroConfigurationFile.exists()) { + LOG.debug("Using {} to load WroConfiguration.", wroConfigurationFile.getPath()); + wroConfigurationAsProperties.load(new FileInputStream(wroConfigurationFile)); + } else { + LOG.warn("Configuration file: '{}' does not exist. Using default configuration.", + wroConfigurationFile); + } + } catch (final IOException e) { + LOG.error("Problem while loading WroConfiguration", e); + throw e; + } } - } catch (final IOException e) { - LOG.error("Problem while loading WroConfiguration", e); - throw e; - } + return wroConfigurationAsProperties; } - return wroConfigurationAsProperties; - } - - /** - * This implementation is similar to the one from Wro4jMojo. TODO: reuse if possible. - */ - private String computeAggregatedFolderPath() { - Validate.notNull(destinationFolder, "DestinationFolder cannot be null!"); - Validate.notNull(getContextFolder(), "ContextFolder cannot be null!"); - final File cssTargetFolder = destinationFolder; - File rootFolder = null; - if (cssTargetFolder.getPath().startsWith(getContextFolder().getPath())) { - rootFolder = getContextFolder(); + + /** + * This implementation is similar to the one from Wro4jMojo. TODO: reuse if + * possible. + */ + private String computeAggregatedFolderPath() { + Validate.notNull(destinationFolder, "DestinationFolder cannot be null!"); + Validate.notNull(getContextFolder(), "ContextFolder cannot be null!"); + final File cssTargetFolder = destinationFolder; + File rootFolder = null; + if (cssTargetFolder.getPath().startsWith(getContextFolder().getPath())) { + rootFolder = getContextFolder(); + } + // compute aggregatedFolderPath + String aggregatedFolderPath = null; + if (rootFolder != null) { + aggregatedFolderPath = StringUtils.removeStart(cssTargetFolder.getPath(), rootFolder.getPath()); + } + LOG.debug("aggregatedFolderPath: {}", aggregatedFolderPath); + return aggregatedFolderPath; } - // compute aggregatedFolderPath - String aggregatedFolderPath = null; - if (rootFolder != null) { - aggregatedFolderPath = StringUtils.removeStart(cssTargetFolder.getPath(), rootFolder.getPath()); + + /** + * Encodes a version using some logic. + * + * @param group + * the name of the resource to encode. + * @param input + * the stream of the result content. + * @return the name of the resource with the version encoded. + */ + private String rename(final String group, final InputStream input) + throws IOException { + return getManagerFactory().create().getNamingStrategy().rename(group, input); } - LOG.debug("aggregatedFolderPath: {}", aggregatedFolderPath); - return aggregatedFolderPath; - } - - /** - * Encodes a version using some logic. - * - * @param group - * the name of the resource to encode. - * @param input - * the stream of the result content. - * @return the name of the resource with the version encoded. - */ - private String rename(final String group, final InputStream input) - throws IOException { - return getManagerFactory().create().getNamingStrategy().rename(group, input); - } - - /** - * This method will ensure that you have a right and initialized instance of {@link StandaloneContextAware}. - */ - private WroManagerFactory getManagerFactory() - throws IOException { - final DefaultStandaloneContextAwareManagerFactory managerFactory = new DefaultStandaloneContextAwareManagerFactory(); - managerFactory.setProcessorsFactory(createProcessorsFactory()); - managerFactory.setNamingStrategy(createNamingStrategy()); - managerFactory.setModelFactory(createWroModelFactory()); - managerFactory.initialize(createStandaloneContext()); - // allow created manager to get injected immediately after creation - return managerFactory; - } - - private NamingStrategy createNamingStrategy() throws IOException { - final ConfigurableNamingStrategy namingStrategy = new ConfigurableNamingStrategy(); - namingStrategy.setProperties(getWroConfigurationProperties()); - return namingStrategy; - } - - private WroModelFactory createWroModelFactory() { - // autodetect if user didn't specify explicitly the wro file path (aka default is used). - notNull(defaultWroFile, "default wroFile cannot be null!"); - final boolean autoDetectWroFile = defaultWroFile.getPath().equals(wroFile.getPath()); - return new SmartWroModelFactory().setWroFile(wroFile).setAutoDetectWroFile(autoDetectWroFile); - } - - private ProcessorsFactory createProcessorsFactory() - throws IOException { - final Properties props = getWroConfigurationProperties(); - if (preProcessorsList != null) { - props.setProperty(ConfigurableProcessorsFactory.PARAM_PRE_PROCESSORS, preProcessorsList); + + /** + * This method will ensure that you have a right and initialized instance of + * {@link StandaloneContextAware}. + */ + private WroManagerFactory getManagerFactory() + throws IOException { + final DefaultStandaloneContextAwareManagerFactory managerFactory = new DefaultStandaloneContextAwareManagerFactory(); + managerFactory.setProcessorsFactory(createProcessorsFactory()); + managerFactory.setNamingStrategy(createNamingStrategy()); + managerFactory.setModelFactory(createWroModelFactory()); + managerFactory.initialize(createStandaloneContext()); + // allow created manager to get injected immediately after creation + return managerFactory; } - if (postProcessorsList != null) { - props.setProperty(ConfigurableProcessorsFactory.PARAM_POST_PROCESSORS, postProcessorsList); + + private NamingStrategy createNamingStrategy() throws IOException { + final ConfigurableNamingStrategy namingStrategy = new ConfigurableNamingStrategy(); + namingStrategy.setProperties(getWroConfigurationProperties()); + return namingStrategy; + } + + private WroModelFactory createWroModelFactory() { + // autodetect if user didn't specify explicitly the wro file path (aka default + // is used). + notNull(defaultWroFile, "default wroFile cannot be null!"); + final boolean autoDetectWroFile = defaultWroFile.getPath().equals(wroFile.getPath()); + return new SmartWroModelFactory().setWroFile(wroFile).setAutoDetectWroFile(autoDetectWroFile); + } + + private ProcessorsFactory createProcessorsFactory() + throws IOException { + final Properties props = getWroConfigurationProperties(); + if (preProcessorsList != null) { + props.setProperty(ConfigurableProcessorsFactory.PARAM_PRE_PROCESSORS, preProcessorsList); + } + if (postProcessorsList != null) { + props.setProperty(ConfigurableProcessorsFactory.PARAM_POST_PROCESSORS, postProcessorsList); + } + return new ConfigurableProcessorsFactory() { + @Override + protected Map newPreProcessorsMap() { + final Map map = super.newPreProcessorsMap(); + // override csslint & jsHint aliases + map.put(CssLintProcessor.ALIAS, new RunnerCssLintProcessor()); + map.put(JsHintProcessor.ALIAS, new RunnerJsHintProcessor(contextFolder)); + return map; + } + + @Override + protected Map newPostProcessorsMap() { + final Map map = super.newPostProcessorsMap(); + // override csslint & jsHint aliases + map.put(CssLintProcessor.ALIAS, new RunnerCssLintProcessor()); + map.put(JsHintProcessor.ALIAS, new RunnerJsHintProcessor(contextFolder)); + return map; + } + }.setProperties(props); + } + + /** + * Creates a {@link StandaloneContext} by setting properties passed after mojo + * is initialized. + */ + private StandaloneContext createStandaloneContext() { + final StandaloneContext runContext = new StandaloneContext(); + runContext.setContextFoldersAsCSV(getContextFolder().getPath()); + runContext.setMinimize(minimize); + runContext.setWroFile(wroFile); + runContext.setIgnoreMissingResourcesAsString(Boolean.toString(ignoreMissingResources)); + return runContext; + } + + /** + * @param destinationFolder + * the destinationFolder to set + * @VisibleForTestOnly + */ + void setDestinationFolder(final File destinationFolder) { + this.destinationFolder = destinationFolder; } - return new ConfigurableProcessorsFactory() { - @Override - protected Map newPreProcessorsMap() { - final Map map = super.newPreProcessorsMap(); - // override csslint & jsHint aliases - map.put(CssLintProcessor.ALIAS, new RunnerCssLintProcessor()); - map.put(JsHintProcessor.ALIAS, new RunnerJsHintProcessor()); - return map; - } - - @Override - protected Map newPostProcessorsMap() { - final Map map = super.newPostProcessorsMap(); - // override csslint & jsHint aliases - map.put(CssLintProcessor.ALIAS, new RunnerCssLintProcessor()); - map.put(JsHintProcessor.ALIAS, new RunnerJsHintProcessor()); - return map; - } - }.setProperties(props); - } - - /** - * Creates a {@link StandaloneContext} by setting properties passed after mojo is initialized. - */ - private StandaloneContext createStandaloneContext() { - final StandaloneContext runContext = new StandaloneContext(); - runContext.setContextFoldersAsCSV(getContextFolder().getPath()); - runContext.setMinimize(minimize); - runContext.setWroFile(wroFile); - runContext.setIgnoreMissingResourcesAsString(Boolean.toString(ignoreMissingResources)); - return runContext; - } - - /** - * @param destinationFolder - * the destinationFolder to set - * @VisibleForTestOnly - */ - void setDestinationFolder(final File destinationFolder) { - this.destinationFolder = destinationFolder; - } } diff --git a/src/main/java/ro/isdc/wro/runner/processor/RunnerJsHintProcessor.java b/src/main/java/ro/isdc/wro/runner/processor/RunnerJsHintProcessor.java index 567c3f1..d0c4257 100644 --- a/src/main/java/ro/isdc/wro/runner/processor/RunnerJsHintProcessor.java +++ b/src/main/java/ro/isdc/wro/runner/processor/RunnerJsHintProcessor.java @@ -1,27 +1,285 @@ package ro.isdc.wro.runner.processor; import ro.isdc.wro.extensions.processor.js.JsHintProcessor; +import ro.isdc.wro.extensions.processor.support.linter.AbstractLinter; import ro.isdc.wro.extensions.processor.support.linter.LinterException; import ro.isdc.wro.model.resource.Resource; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Map; +import java.util.regex.Pattern; -/** - * Custom extension of {@link JsHintProcessor} created for wro4j-runner. - * - * @author Alex Objelean - * @since 1.7.2 - */ -public class RunnerJsHintProcessor - extends JsHintProcessor { - /** - * Override the alias of original jsHint processor implementation. - */ - public static String ALIAS = JsHintProcessor.ALIAS; - @Override - protected void onLinterException(final LinterException e, final Resource resource) { - super.onLinterException(e, resource); - System.err.println("The following resource: " + resource + " has " + e.getErrors().size() + " errors."); - System.err.println(e.getErrors()); - throw e; - } -} +import org.omg.CORBA.SystemException; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import ro.isdc.wro.extensions.processor.support.linter.LinterError; + +public class RunnerJsHintProcessor extends JsHintProcessor { + public static String ALIAS = JsHintProcessor.ALIAS; + private final File contextFolder; // <-- AGREGA ESTA LÍNEA + + public RunnerJsHintProcessor(File contextFolder) { + super(); + this.contextFolder = contextFolder; + } + + public RunnerJsHintProcessor() { + this(new File(System.getProperty("user.dir"))); + } + + @Override + protected void onLinterException(final LinterException e, final Resource resource) { + // super.onLinterException(e, resource); + System.out.println("The following resource: " + (resource != null ? resource.getUri() : "null") + " has " + + e.getErrors().size() + " errors."); + System.out.println("ERRORS:"); + for (Object err : e.getErrors()) { + String errStr = err.toString(); + // if (errStr.contains("reason")) { + // // Extrae los campos manualmente del string + // String line = extractField(errStr, "line"); + // String character = extractField(errStr, "character"); + // String reason = extractField(errStr, "reason"); + // String evidence = extractField(errStr, "evidence"); + // System.out.println( + // " Line: " + line + "\n Char: " + character + "\n Reason: " + reason + "\n + // Code: " + // + evidence); + if (err instanceof LinterError) { + LinterError linterError = (LinterError) err; + System.out.println( + "[\n Line: " + linterError.getLine() + + "\n Char: " + linterError.getCharacter() + + " \n Reason: " + linterError.getReason() + + (linterError.getEvidence() != null && !linterError.getEvidence().isEmpty() + ? "\n Code: " + linterError.getEvidence() + : "")); + System.out.println("],"); + } else { + System.err.println(err); + } + } + + // Detecta si estamos en un entorno de test (JUnit/Surefire) + boolean inTest = false; + for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { + String cls = ste.getClassName(); + if (cls.startsWith("org.junit.") || cls.startsWith("org.apache.maven.surefire.")) { + inTest = true; + break; + } + } + if (inTest) { + throw e; + } + } + + private File getResourceFile(Resource resource) { + if (resource == null || resource.getUri() == null) { + return null; + } + File file = new File(resource.getUri()); + if (file.exists()) { + return file; + } + if (this.contextFolder != null) { + String relativePath = resource.getUri().replaceFirst("^/", ""); + file = new File(this.contextFolder, relativePath); + if (file.exists()) { + return file; + } + } else { + System.out.println("[JSHint] contextFolder not defined in System properties"); + } + System.out.println("[JSHint] getResourceFile: No file found for resource.getUri(): " + resource.getUri()); + return null; + } + + /** + * Busca .jshintrc desde el directorio inicial hacia arriba hasta contextFolder + * o la raíz. + */ + private Map findAndLoadJshintrc(File startDir) { + File dir = startDir.getAbsoluteFile(); // <-- Asegura que sea absoluto + while (dir != null) { + File jshintrc = new File(dir, ".jshintrc"); + if (jshintrc.exists()) { + FileReader reader = null; + try { + reader = new FileReader(jshintrc); + Gson gson = new Gson(); + Type type = new TypeToken>() { + }.getType(); + System.out.println("[JShint] .jshintrc loaded: " + jshintrc.getAbsolutePath()); + return gson.fromJson(reader, type); + } catch (IOException e) { + System.err.println("[JShint] No se pudo leer .jshintrc en " + jshintrc.getAbsolutePath() + ": " + + e.getMessage()); + break; + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignore) { + } + } + } + } + dir = dir.getParentFile(); + } + return null; + } + + // Convierte el Map de opciones a CSV: key=value,key2=value2 + private String mapToCsvOptions(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) + sb.append(","); + if (entry.getValue() instanceof Map) { + // Generaliza: cualquier key cuyo valor sea un objeto, conviértelo a array + sb.append(entry.getKey()).append("=["); + Map objMap = (Map) entry.getValue(); + boolean first = true; + for (Map.Entry gEntry : objMap.entrySet()) { + String key = gEntry.getKey(); + String value = gEntry.getValue() != null ? gEntry.getValue().toString() : "true"; + if (value.equals("true") || value.equals(true)) { + if (!first) + sb.append(","); + sb.append("'").append(key).append("'"); + first = false; + } + } + sb.append("]"); + } else { + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + } + return sb.toString(); + } + + private String findAndLoadJshintrcOptionsAsCsv(File searchDir) { + Map optionsMap = findAndLoadJshintrc(searchDir); + if (optionsMap != null) { + return mapToCsvOptions(optionsMap); + } + return null; + } + + private String stripComments(String code) { + // Elimina comentarios de bloque /* ... */ + code = code.replaceAll("(?s)/\\*.*?\\*/", ""); + // Elimina comentarios de línea // + code = code.replaceAll("(?m)//.*?$", ""); + return code; + } + + private String stripStrings(String code) { + // Elimina strings dobles y simples (no soporta backticks) + code = code.replaceAll("\"(?:\\\\.|[^\"\\\\])*\"", "\"\""); + code = code.replaceAll("'(?:\\\\.|[^'\\\\])*'", "''"); + return code; + } + + private String getContentWithInjectedGlobals(String jsHintOptions, String originalContent) { + if (jsHintOptions == null) { + return originalContent; + } + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("globals=\\[([^\\]]*)\\]") + .matcher(jsHintOptions); + if (!m.find()) { + return originalContent; + } + String globalsList = m.group(1); + StringBuilder sb = new StringBuilder(); + + // Elimina comentarios antes de buscar las globals usadas + String code = stripComments(originalContent); + code = stripStrings(code); + + for (String g : globalsList.split(",")) { + String key = g.trim().replace("'", ""); + if (!key.isEmpty()) { + // Busca la global como identificador independiente, ignorando strings y + // propiedades + // Coincide solo si la palabra aparece como identificador (no precedida de punto + // ni dentro de comillas) + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( + "(? - /js/*.js - /css/*.css + /**.js + /**.css - /oocss/**.css + /**.css diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..b2e138a --- /dev/null +++ b/test.sh @@ -0,0 +1,6 @@ +JAR_FILE=$(ls target/wro4j-runner-*.jar | grep -v 'sources' | head -n 1) + +java -jar "$JAR_FILE" \ + --wroFile src/test/resources/ro/isdc/wro/runner/wro.xml \ + --contextFolder src/test/resources/ro/isdc/wro/runner \ + --preProcessors jsHint \ No newline at end of file